Salesforce Flowを呼び出し可能なApexで強化する:開発者向けガイド

執筆者:Salesforce 開発者


背景と適用シナリオ

Salesforce Flowは、Salesforceプラットフォームにおける主要な宣言的自動化ツールです。コードを記述することなく、複雑なビジネスプロセスを自動化できるため、多くのSalesforce管理者やコンサルタントにとって強力な武器となっています。しかし、どれだけFlowが強力であっても、宣言的なツールだけでは対応が難しい要件に直面することがあります。

例えば、以下のようなシナリオです:

  • 外部システムとの連携: REST APIを呼び出して外部の住所検証サービスを利用したり、ERPシステムから最新の在庫情報を取得したりする場合。
  • 複雑なビジネスロジック: 標準の数式関数では実現できない、高度な数学的計算や独自のアルゴリズムに基づくデータ処理。
  • 一括処理とパフォーマンス: 数千件のレコードに対して、関連オブジェクトをまたいだ複雑な更新処理を、効率的かつガバナ制限(Governor Limits)を考慮しながら実行する必要がある場合。
  • 宣言的ツールではアクセスできないオブジェクトやメタデータの操作。

このような課題に直面したとき、Salesforce開発者の出番です。Invokable Apex(呼び出し可能なApex)は、Flowの宣言的な能力とApexのプログラム的な柔軟性を繋ぐ完璧な架け橋となります。Invokable Apexを使用することで、開発者は再利用可能なApexロジックを作成し、それをFlowの「アクション」要素として管理者に提供できます。これにより、管理者はコードを意識することなく、Flowのキャンバス上でその強力な機能をドラッグ&ドロップで利用できるようになります。

この記事では、Salesforce開発者の視点から、Invokable Apexの原理を解説し、具体的な実装例を通じて、Flowの可能性を最大限に引き出す方法を探求します。


原理説明

Invokable Apexの核心は、特定のアノテーション(Annotation)を使用することで、ApexメソッドをFlowから呼び出し可能にする仕組みです。主要なアノテーションは @InvocableMethod@InvocableVariable の2つです。

@InvocableMethod

このアノテーションを付与されたメソッドが、Flowから呼び出されるエントリーポイントとなります。このアノテーションにはいくつかの重要な制約と特徴があります。

  • 静的メソッド(static method): 呼び出し可能なメソッドは、必ず static として定義する必要があります。
  • 単一のパラメータ: メソッドはパラメータを1つしか受け取ることができません。
  • リスト型のパラメータ: その唯一のパラメータは、必ずリスト型(List<T>)でなければなりません。T には、プリミティブ型(String, ID, Integerなど)、sObject型(Account, Contactなど)、または後述するカスタムの内部クラスを指定できます。このリスト構造は、Flowが本質的に一括処理(Bulkification)を前提としているためです。Flowが1つのレコードで起動された場合でも、要素が1つのリストとしてApexに渡されます。
  • リスト型の戻り値: メソッドが値を返す場合、その戻り値もリスト型(List<U>)である必要があります。戻り値のリストの要素数は、入力リストの要素数と一致していなければなりません。これにより、Flowはどの入力がどの出力に対応するのかを正確にマッピングできます。
  • 属性:
    • label: Flowの要素選択画面に表示されるアクションの名前。分かりやすい名前を付けることが重要です。
    • description: アクションの説明文。管理者がその機能を理解するのに役立ちます。
    • category: Flowの要素選択画面で、アクションをグループ化するためのカテゴリ名。
    • callout: 外部サービスへのコールアウト(HTTPリクエストなど)を行う場合は true に設定する必要があります。デフォルトは false です。

@InvocableVariable

FlowとApex間で複雑なデータをやり取りするために、カスタムの内部クラス(ラッパークラス)を定義することがよくあります。このクラスのプロパティをFlowの入力・出力変数としてマッピングするために @InvocableVariable アノテーションを使用します。

  • 対象: カスタム内部クラスのメンバー変数に付与します。
  • 属性:
    • label: Flowの変数マッピング画面に表示される変数名。
    • description: 変数の説明。
    • required: この変数が入力として必須かどうかを指定します(true/false)。

これらのアノテーションを組み合わせることで、開発者はFlowの管理者にとって直感的で使いやすいカスタムアクションを作成できます。入力と出力が明確に定義された再利用可能なコンポーネントとしてApexロジックをカプセル化することが、Invokable Apex設計の鍵となります。


サンプルコード

ここでは、取引先(Account)のIDリストを受け取り、各取引先の年間売上(AnnualRevenue)に基づいて「顧客セグメント」を判定し、結果を返すInvokable Apexを作成します。このアクションは、例えばケースが作成された際に、関連する取引先の重要度を自動で判定してケースに付与する、といったFlowで利用できます。

このコードはSalesforce公式ドキュメントの設計思想に基づいています。

public class AccountSegmentAction {

    // Flowからの入力を受け取るための内部クラス
    // 1つのリクエストが1つのインスタンスに対応します
    public class AccountSegmentRequest {
        @InvocableVariable(label='Account ID' description='セグメントを判定したい取引先のID' required=true)
        public ID accountId;
    }

    // Flowへ結果を返すための内部クラス
    // 1つの結果が1つのインスタンスに対応します
    public class AccountSegmentResult {
        @InvocableVariable(label='Account ID' description='処理対象となった取引先のID')
        public ID accountId;

        @InvocableVariable(label='Customer Segment' description='判定された顧客セグメント')
        public String segment;

        @InvocableVariable(label='Error Message' description='エラーが発生した場合のメッセージ')
        public String errorMessage;
    }

    /**
     * @description 取引先IDのリストを受け取り、年間売上に基づいて顧客セグメントを判定して返す
     * @param requests Flowから渡される入力変数のリスト
     * @return 処理結果のリスト
     */
    @InvocableMethod(label='Get Account Segment' description='取引先の年間売上に基づいて顧客セグメントを判定します' category='Account Utils')
    public static List<AccountSegmentResult> getAccountSegment(List<AccountSegmentRequest> requests) {
        // 結果を格納するためのリストを初期化
        List<AccountSegmentResult> results = new List<AccountSegmentResult>();
        
        // 入力リクエストから取引先IDのセットを作成
        Set<ID> accountIds = new Set<ID>();
        for (AccountSegmentRequest req : requests) {
            accountIds.add(req.accountId);
        }

        // 必要な項目(AnnualRevenue)のみを取得するSOQLクエリ
        Map<ID, Account> accountMap = new Map<ID, Account>([
            SELECT Id, Name, AnnualRevenue 
            FROM Account 
            WHERE Id IN :accountIds
        ]);

        // 各リクエストをループ処理
        for (AccountSegmentRequest req : requests) {
            // 出力用のインスタンスを作成
            AccountSegmentResult result = new AccountSegmentResult();
            result.accountId = req.accountId;

            try {
                if (accountMap.containsKey(req.accountId)) {
                    Account acc = accountMap.get(req.accountId);
                    Decimal revenue = acc.AnnualRevenue;

                    // 年間売上に基づいてセグメントを判定するロジック
                    if (revenue == null) {
                        result.segment = 'Unknown';
                    } else if (revenue >= 1000000000) {
                        result.segment = 'Enterprise';
                    } else if (revenue >= 100000000) {
                        result.segment = 'Mid-Market';
                    } else {
                        result.segment = 'SMB';
                    }
                } else {
                    // クエリで見つからなかった場合
                    result.segment = 'Not Found';
                    result.errorMessage = '指定されたIDの取引先が見つかりませんでした。';
                }
            } catch (Exception e) {
                // 予期せぬエラーをキャッチ
                result.errorMessage = 'エラーが発生しました: ' + e.getMessage();
            }
            results.add(result);
        }

        // 処理結果のリストをFlowに返す
        return results;
    }
}

コードの解説

  • AccountSegmentRequest クラス: Flowからの入力を定義します。ここでは accountId を受け取ります。@InvocableVariableアノテーションにより、Flowの画面で「Account ID」というラベルで表示され、入力が必須となります。
  • AccountSegmentResult クラス: Flowへの出力を定義します。セグメント判定の結果(segment)、元の取引先ID(accountId)、そしてエラーハンドリング用のメッセージ(errorMessage)を返します。
  • getAccountSegment メソッド: メインの処理ロジックです。@InvocableMethodアノテーションにより、このメソッドがFlowから呼び出し可能になります。
    • 一括処理(Bulkification): まず、入力リストからIDのSetを作成し、1回のSOQL(Salesforce Object Query Language)クエリで全ての必要な取引先データを取得します。これにより、ループ内でクエリを発行することを避け、ガバナ制限に準拠しています。
    • ロジック: 取得した取引先のAnnualRevenueフィールドの値に応じて、セグメント('Enterprise', 'Mid-Market', 'SMB')を決定します。
    • エラーハンドリング: try-catchブロックを使い、予期せぬ例外が発生してもFlow全体が停止しないようにしています。また、IDが見つからないケースも考慮し、エラーメッセージを結果に含めて返すことで、Flow側で後続の処理を分岐させることが可能になります。

注意事項

Invokable Apexを実装・運用する際には、いくつかの重要な点に注意する必要があります。

権限 (Permissions)

Flowを実行するユーザーは、呼び出すApexクラスへの実行権限が必要です。プロファイルや権限セットで、対象のApexクラスへのアクセスを許可してください。また、Flowの実行コンテキスト(ユーザーコンテキストかシステムコンテキストか)によって、Apex内で実行されるSOQLやDML(Data Manipulation Language)操作が参照するデータや実行可否が変わる点にも注意が必要です。

API制限 (API Limits) とガバナ制限 (Governor Limits)

Invokable Apexも、通常のApexと同様にガバナ制限の対象となります。1つのトランザクション内で実行できるSOQLクエリの数(100回)、DMLステートメントの数(150回)、CPU時間などに上限があります。サンプルコードで示したように、ループ内でSOQLやDMLを実行することは絶対に避け、常に一括処理を念頭に置いたコーディングを心がけてください。

エラー処理 (Error Handling)

Apex内でハンドルされない例外が発生すると、Flowは失敗し停止します。これはユーザー体験を損なう原因となります。必ずtry-catchブロックを使用して例外を捕捉し、エラー情報をログに記録したり、出力変数(例:errorMessage)を通じてFlowに返すように設計してください。Flow側では、このエラーメッセージを元に「決定」要素で処理を分岐させたり、エラー内容を画面に表示したり、システム管理者に通知したりといった対応が可能になります。

コールアウト (Callouts)

外部APIを呼び出す場合、メソッドに @InvocableMethod(callout=true) を指定する必要があります。コールアウトを含むトランザクションでは、コールアウトの前に未確定のDML操作(insert, updateなど)を実行することはできません。もしDMLとコールアウトの両方が必要な場合は、トランザクションを分割するなどの工夫が必要になります。


まとめとベストプラクティス

Invokable Apexは、Salesforce開発者が宣言的ツールの限界を打ち破り、より高度で柔軟な自動化ソリューションを構築するための非常に強力な機能です。管理者が再利用可能なコンポーネントとして安全に利用できるカスタムアクションを提供することで、組織全体の生産性を飛躍的に向上させることができます。

以下に、Invokable Apexを開発する上でのベストプラクティスをまとめます。

  1. 汎用性と再利用性を意識する: 特定のFlowに依存したロジックではなく、様々なシナリオで再利用できるような汎用的なアクションとして設計しましょう。例えば、「取引先セグメントを取得する」アクションは、ケース処理、商談管理、マーケティングオートメーションなど、多くのFlowで役立ちます。
  2. 入力と出力の契約を明確にする: @InvocableMethod@InvocableVariablelabeldescriptionを丁寧に記述してください。これにより、管理者がコードを読むことなく、アクションの機能と使い方を正しく理解できます。
  3. 徹底的な一括処理(Bulkification): メソッドは常に複数のレコードを処理することを前提として実装してください。これがパフォーマンスを維持し、ガバナ制限を回避するための最も重要な原則です。
  4. 堅牢なエラーハンドリングを実装する: 予期せぬデータや状況でもFlowがクラッシュしないよう、エラーを捕捉し、意味のあるフィードバックをFlowに返す仕組みを組み込みましょう。
  5. 宣言的な解決策を優先する: Apexは強力ですが、メンテナンス性や開発生産性の観点からは、可能な限りFlowの標準機能で解決できないか検討することが先決です。Invokable Apexは、あくまで「最後の手段」または「最適な手段」として活用しましょう。

これらのプラクティスに従うことで、FlowのパワーとApexの柔軟性を組み合わせた、スケーラブルで保守性の高い、優れた自動化ソリューションを提供できるはずです。

コメント