> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://help.moveworks.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://help.moveworks.com/_mcp/server.

# Fan Out and Synthesize

Query independent systems at the same time, then use an [LLM Action](/agent-studio/actions/llm-actions) to turn their combined results into one answer like a summary, a priority call, a recommendation that no single source had on its own.

If the results just need to be combined or calculated, skip the LLM and use [DSL](/agent-studio/core-platform/configuration-languages/moveworks-dsl-reference) (Moveworks' data-mapping syntax) in [`return`](/agent-studio/actions/compound-actions/return) directly.

## Context and problem

A complete picture often doesn't live in one system: a CRM record, a ticketing system, and a usage or delivery tracker might each hold part of the story, and none of them can produce the full picture alone. This pattern fetches from all of them at once, then reasons across the results to produce that picture.

For example, combining a CRM record (e.g. Salesforce), open ITSM incidents (e.g. ServiceNow), and Product Management delivery status (e.g. Asana or Jira) into a renewal briefing that explains risk, recent changes, and talking points for a Customer Success Manager. The same shape applies to other combinations of systems and synthesized output like:

* **Onboarding readiness check** — combine identity status (e.g. Okta), hardware provisioning (e.g. an asset system), and software/access grants into a single summary of what's still blocking a new hire from being productive.
* **Incident briefing** — combine active monitoring alerts, recent change records, and on-call ownership into a short narrative of what's likely related and who should respond, instead of making the on-call engineer piece it together from three dashboards.

## Solution

A [Compound Action](/agent-studio/actions/compound-actions) lets you define a sequence of steps that runs on its own, with no user input needed partway through. Think of it as a container that holds every step below and runs them in order.

Inside that Compound Action, fetch the data from each system in parallel, through separate branches, so all the calls go out at once instead of one after another. Once every branch is done, run one [LLM Action](/agent-studio/actions/llm-actions) that reads the combined results and writes the synthesized answer, then send that back with `return`.

You can build that sequence visually in the [Low Code Editor](/agent-studio/actions/compound-actions#getting-started) or write the YAML shown below directly; the Editor and the YAML stay in sync, so go with the option you prefer.

```mermaid
flowchart LR
  Request["Input: account ID + briefing request"] --> Validate["Validate inputs"]
  subgraph CA["Compound Action: build briefing"]
    Validate --> FanOut{{"parallel"}}
    FanOut --> CRM["CRM Action<br />output_key: crm_result"]
    FanOut --> ITSM["ITSM Action<br />output_key: itsm_result"]
    FanOut --> PMProjects["PM Projects Action<br />output_key: pm_projects_result"]
    FanOut --> PMTasks["PM Tasks Action<br />output_key: pm_tasks_result"]
    CRM --> Synthesize["LLM Action<br />input_args select + shape<br />the four branch outputs"]
    ITSM --> Synthesize
    PMProjects --> Synthesize
    PMTasks --> Synthesize
    Synthesize --> Result["return.output_mapper<br />briefing + source status"]
  end
```

The pattern is composed of the following components:

* [`parallel` with branches](/agent-studio/actions/compound-actions/parallel) runs each source query at the same time and waits for all of them to finish before moving on.
* An [LLM Action](/agent-studio/actions/llm-actions) does the synthesis.
  * Use `generate_text_action` when the deliverable is prose (a briefing, a summary).
  * Use `generate_structured_value_action` when a later step needs the LLM's judgment as data, like a computed risk level and a list of talking points, rather than the source records.
* [`return`](/agent-studio/actions/compound-actions/return) shapes what gets sent back — the synthesized output plus which sources succeeded.

## When to use this pattern

Use fan out and synthesize when **all** of the following hold:

* You need data from **two or more independent sources**. No source depends on another's output.
* The sources can be fetched **all at once**, and fetching them one after another would be needlessly slow.
* Getting the real insight requires **understanding what the data means together**, not just merging it. That could be summarizing, prioritizing, explaining, or drawing a conclusion across sources.

## When not to use this pattern

* **You only need the source records themselves, unchanged.** If the consumer just needs each system's data reshaped into a specific format, with no judgment involved, use [`parallel`](/agent-studio/actions/compound-actions/parallel) and shape the raw results straight into [`return`](/agent-studio/actions/compound-actions/return).
* **The combination is deterministic.** Summing numbers, concatenating fields, or checking the status of fields (like whether identity, hardware, and access statuses are all green in an onboarding flow) doesn't need an LLM to interpret meaning — DSL and data mappers can express that logic directly. Use them in the return mapper instead. See [LLM vs DSL](/agent-studio/guides/getting-started/decision-frameworks#llm-vs-dsl).
* **The sources are dependent.** If source B needs a value that source A returns, you can't fan out — run them one after another in the same Compound Action instead (see the [Golden Rule](/agent-studio/guides/best-practices/the-golden-rule)).

## Example: renewal briefing

A [Compound Action](/agent-studio/actions/compound-actions) pulls a CRM record, ITSM incidents, and Product Management delivery status in parallel, then synthesizes a briefing for a renewal call. The example runs against [Purple Suite](/agent-studio/quickstart-guide/purple-suite-setup), which comes with built-in sample data for each of these systems:

* **CRM (account record)** — Purple Suite's own CRM app, standing in for something like **Salesforce**.
* **ITSM (incidents)** — Purple Suite's own ITSM app, standing in for something like **ServiceNow**.
* **Product Management (projects/tasks)** — Purple Suite's own PM app, standing in for something like **Asana** or **Jira**.

The overall structure and flow here is what we used for our Purple Suite example. You will need to define your own actions and swap them in to retrieve data from whichever systems make sense for your workflow.

We should configure a couple of Input Arguments on the Compound Action to feed it the data it needs: `account_id` and `account_name`. Mark both as **required** in the Input Args panel. That way, Agent Studio itself blocks the Compound Action from running with either one missing or blank, instead of letting it run and quietly return a wrong briefing later on (see Step 1 for why an empty `account_name` in particular is a problem for our particular workflow).

### Step 1: Fan out to the four sources, resiliently

If you're building this in the [Low Code Editor](/agent-studio/actions/compound-actions#getting-started), you can add `parallel`, `try_catch`, and `action` steps from its step picker avoid using YAML.

This is effectively what the editor would generate for you, letting us share the shape of the process easily.

The `parallel` block queries CRM, ITSM, and Product Management (projects and tasks) at the same time, instead of one after another. The same three-step shape repeats four times, with the action name and `output_key` changing between branches.

Each branch is wrapped in [`try_catch`](/agent-studio/actions/compound-actions/try-catch-and-raise-error): if one source fails (say, ITSM is down), it doesn't stop the other three from returning and the briefing from being built. The `catch` records the failure with a short, reusable [`script`](/agent-studio/actions/compound-actions/compound-action-syntax-reference#script-execute-scripts) step you can copy as-is for every branch.

`data.account_id` and `data.account_name` refer to the input arguments configured above. The CRM branch scopes its query with `account_id` directly. Purple Suite's ITSM and PM apps don't use the account ID as a foreign key, but each incident/project/task's `title`/`name` includes the account's name. It is better to retrieve only the data that we need, rather than gathering additional unecessary data, so we therefore filter the data at source using the `$filter` query parameter avaialble for that API endpoint.

While this might sound like an implementation detail for the Purple Suite, it's important consideration when you integrate with your own systems and the available API endpoints. Filtering data before it reaches the LLM, is more reliable than asking the LLM to sift a full, unfiltered table itself. However, if we passed an empty `account_name` to the API, an empty filter value would match nothing (or everything), instead of just this account's records. That's why we have required `account_name` as an input argument in this example.

You can create each branch's HTTP Action by [importing the cURL command](/agent-studio/actions/http-actions#import-from-curl) below, naming it to match the `action_name` used in the YAML above. Purple Suite seeds four fixed accounts (`ACC-0001` through `ACC-0004`) with matching ITSM incidents and PM projects/tasks, so you can try these against the Purple Suite endpoints right away.

```bash title="get_crm_account"
curl -X GET 'https://marketplace.moveworks.com/api/purple-suite/crm/accounts/{{account_id}}' \
  -H 'Authorization: Bearer YOUR_PAT' \
  -H 'X-Instance-ID: YOUR_INSTANCE_ID'
```

```bash title="get_itsm_incidents"
curl -X GET "https://marketplace.moveworks.com/api/purple-suite/itsm/incidents?$filter=contains(title,'{{account_name}}')" \
  -H 'Authorization: Bearer YOUR_PAT' \
  -H 'X-Instance-ID: YOUR_INSTANCE_ID'
```

```bash title="get_pm_projects"
curl -X GET "https://marketplace.moveworks.com/api/purple-suite/pm/projects?$filter=contains(name,'{{account_name}}')" \
  -H 'Authorization: Bearer YOUR_PAT' \
  -H 'X-Instance-ID: YOUR_INSTANCE_ID'
```

```bash title="get_pm_tasks"
curl -X GET "https://marketplace.moveworks.com/api/purple-suite/pm/tasks?$filter=contains(title,'{{account_name}}')" \
  -H 'Authorization: Bearer YOUR_PAT' \
  -H 'X-Instance-ID: YOUR_INSTANCE_ID'
```

You can retrieve your Personal Access Token (PAT) and instance ID from the Purple Suite [Credentials page](https://marketplace.moveworks.com/purple-suite/settings?section=credentials). Remember that a PAT is a secret credential, and you should make sure you handle it securely, only saving it in the secure inputs. The `{{...}}` placeholders get filled in from the matching `input_args` value in the YAML below — `account_id` from `data.account_id`, `account_name` from `data.account_name` — the same way `{{title}}` and friends work in the [Categorize and Route](/agent-studio/guides/architecture/orchestration-patterns/categorize-and-route#step-3-create-the-ticket-in-purple-suite) example.

```yaml title="Compound Action: build_renewal_briefing (fan out)"
steps:
  - parallel:
      branches:
        - steps:
            - try_catch:
                try:
                  steps:
                    - action:
                        action_name: get_crm_account
                        output_key: crm_result
                        input_args:
                          account_id: data.account_id
                catch:
                  steps:
                    - script:
                        output_key: crm_result # Reusing the same key
                        code: "{'error': 'CRM system unavailable or account not found'}"
        - steps:
            - try_catch:
                try:
                  steps:
                    - action:
                        action_name: get_itsm_incidents
                        output_key: itsm_result
                        input_args:
                          account_name: data.account_name
                catch:
                  steps:
                    - script:
                        output_key: itsm_result
                        code: "{'error': 'ITSM system unavailable'}"
        - steps:
            - try_catch:
                try:
                  steps:
                    - action:
                        action_name: get_pm_projects
                        output_key: pm_projects_result
                        input_args:
                          account_name: data.account_name
                catch:
                  steps:
                    - script:
                        output_key: pm_projects_result
                        code: "{'error': 'PM Projects unavailable'}"
        - steps:
            - try_catch:
                try:
                  steps:
                    - action:
                        action_name: get_pm_tasks
                        output_key: pm_tasks_result
                        input_args:
                          account_name: data.account_name
                catch:
                  steps:
                    - script:
                        output_key: pm_tasks_result
                        code: "{'error': 'PM Tasks unavailable'}"
```

### Step 2: Synthesize with an LLM Action

A [`generate_text_action`](/agent-studio/actions/built-in-actions) reads the results from all four systems, decides what they mean together and writes the briefing. Because the HTTP actions already filter the data by the account name, the LLM only needs to reason about what the (already-relevant) results mean — it isn't asked to filter anything itself. The prompt tells it what to reason about and how to handle a missing source. `RENDER()` builds the text sent to the LLM. Each `{{...}}` placeholder in the `template` gets filled in with the matching value listed under `args`, while `$STRINGIFY_JSON` converts a branch's result into readable text first:

```yaml title="Synthesize a prose briefing"
  - action:
      action_name: mw.generate_text_action
      output_key: briefing
      input_args:
        system_prompt: >-
          '"You are a renewal analyst. Evaluate evidence across CRM, ITSM,
          and PM (each has already been scoped to this account). Describe
          time trends, severity/status, delivery trajectory, counter-signals,
          and uncertainty, and recommend a renewal posture. Do not claim
          incidents caused churn; distinguish association from proof. If any
          source is unavailable, label the briefing PARTIAL and say which
          evidence is missing."'
        temperature: '0.2'
        user_input:
          RENDER():
            template: |
              ACCOUNT NAME: {{account_name}}

              CRM:
              {{crm}}

              ITSM:
              {{itsm}}

              PM PROJECTS:
              {{projects}}

              PM TASKS:
              {{tasks}}
            args:
              account_name: data.account_name
              crm: $STRINGIFY_JSON(data.crm_result)
              itsm: $STRINGIFY_JSON(data.itsm_result)
              projects: $STRINGIFY_JSON(data.pm_projects_result)
              tasks: $STRINGIFY_JSON(data.pm_tasks_result)
```

If a later step needs the LLM's judgment as data instead of prose (like a computed risk level or a list of talking points), use `generate_structured_value_action` with a schema instead — see [LLM Actions](/agent-studio/actions/llm-actions).

### Step 3: Return the result

`return` is the step that sends the finished result back out of the Compound Action.

A source can fail quietly earlier in the run, so before sending that result off, it's worth telling the user which sources actually came through. That's why we define a `sources_available` property in the result. Without that, the user would have no way to tell the difference between "CRM had nothing to report" and "CRM never responded." Because the catch step reuses the try step's `output_key`, `data.crm_result` (and the other three) is always populated, either with the real result or with the `{'error': ...}` object the catch step wrote instead. Checking `.error == NULL` tells us which one it is.

```yaml title="Return"
  - return:
      output_mapper:
        account_id: data.account_id
        account_name: data.account_name
        analysis: data.briefing.generated_output
        sources_available:
          crm: data.crm_result.error == NULL
          itsm: data.itsm_result.error == NULL
          pm_projects: data.pm_projects_result.error == NULL
          pm_tasks: data.pm_tasks_result.error == NULL
```

To try this yourself, [set up a Purple Suite instance](/agent-studio/quickstart-guide/purple-suite-setup), then create these steps as a new Compound Action in the Low Code Editor, along with your data query actions.

Purple Suite seeds four fixed accounts, `ACC-0001` through `ACC-0004`, each with its own consistent story across CRM, ITSM, and PM. Their names are randomly generated per instance, so we can't provide a list of account names here. Look up each account's `name` in the CRM app (or via `get_crm_account`) first. Run the Compound Action once per account and compare the briefings: since each account has a different mix of incidents and delivery status behind it, you should see a different renewal assessment for each one, ranging from strong renewal .

## Issues and considerations

A few things worth knowing before you build this yourself:

* **The LLM Action does its own shaping.** There's no separate mapping step between `parallel` and the LLM Action; its `input_args` are where you select and format the branch results. If a value isn't plain text already, turn it into text first, like the example does with `$STRINGIFY_JSON`.
* **Give every branch its own `output_key`.** That's the name each branch's result gets stored under once it finishes, and it's how you read that result back later as `data.<output_key>`.
* **One failed source can take down the whole thing.** Left alone, an error in any branch can stop the entire Compound Action, so the requester gets an error instead of an answer. Wrapping a branch in [`try_catch`](/agent-studio/actions/compound-actions/try-catch-and-raise-error) lets the others keep going, so you can still return a partial result and say which sources were missing.
* **You only move as fast as your slowest branch.** The branches run at once, but each finishes on its own schedule, and the next step doesn't start until every branch has finished. So a single slow or high-timeout source holds up the whole thing, even though the others were done long before it.
* **Match the LLM Action to what happens next:**
  * Use `generate_text_action` when creating prose that a person will read.
  * Use `generate_structured_value_action` when a later step requires a result with the LLM's judgement and a specific schema, returning exactly the fields you expect.
* **Filter at the source whenever you can.** If a system lets you scope its results, do it there instead of sending unecessary information to the LLM. It's more reliable finding the right answer in a small, relevant set of data than searching for it in a large, unfiltered one ([here's why](/agent-studio/guides/best-practices/the-golden-rule#the-lost-in-the-middle-problem)). Even without a foreign key back to the account, this example's ITSM/PM branches use each endpoint's `$filter` query parameter to match on the account name, so the LLM never sees another account's records in the first place. If a branch's `$filter` value is ever wrong or missing, the giveaway is a briefing whose counts match the whole table rather than just this account's records — check for that if the numbers look too high.
* **Watch what data reaches the LLM.** If a branch pulls in content you don't fully control, like a web page or an uploaded file, it could carry hidden instructions that hijack the LLM (a prompt injection). That becomes dangerous once a later step can send data back out. See [Security guidance](/agent-studio/actions/llm-actions#security-the-lethal-trifecta) for how to stay safe.

## Related guidance

#### [Parallel](/agent-studio/actions/compound-actions/parallel)

Run a fixed set of different branches at once, or parallelize a `for` loop when every item in a collection needs the same steps.

#### [LLM Actions](/agent-studio/actions/llm-actions)

Use `generate_text_action` for prose, or `generate_structured_value_action` when a downstream step needs a specific, predictable set of fields back.

#### [Return](/agent-studio/actions/compound-actions/return)

Shape what gets sent back as the final result.

#### [Try Catch and Raise Error](/agent-studio/actions/compound-actions/try-catch-and-raise-error)

Contain source failures when a partial synthesis is still useful.

#### [LLM vs DSL](/agent-studio/guides/getting-started/decision-frameworks#llm-vs-dsl)

Decide when combining data needs an LLM at all.