> ## Documentation Index
> Fetch the complete documentation index at: https://docs.attention.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Agent Builder

> Build the workflow behind an Attention agent — trigger, steps, loops, routers and code — then validate and test it, all from an MCP client.

The Agent Builder tools let an assistant author the flow behind an Attention agent: what fires it, which steps run, and how data passes between them. These are the same flows you build on the [Workflow Builder](/builder-101/getting-started) canvas, so anything built here can be opened and edited in the Attention UI.

<Note>
  **Availability.** Agent Builder is rolled out per organization. When it is not enabled for your workspace, these tools do not appear in your client's tool list. Every tool also requires the **admin** role or the **Product** role.
</Note>

<Warning>
  **Everything you build is a draft.** There is no publish tool. To make an agent run for real, open it in the Attention UI and publish it there.
</Warning>

## How a build goes

<Steps>
  <Step title="Create the agent">
    [`create_agent_flow`](#create_agent_flow) creates the agent and returns its `agent_id` plus the `flow_id` of its empty flow.
  </Step>

  <Step title="Look up each piece before using it">
    [`get_piece_schema`](#get_piece_schema) returns the exact trigger, action and property ids. Use those ids, not display names. Most broken steps come from a property name guessed from its label.
  </Step>

  <Step title="Set the trigger, then add steps">
    [`set_trigger`](#set_trigger) picks what fires the flow. [`add_step`](#add_step) appends piece actions. For control flow, use [`add_loop_step`](#add_loop_step), [`add_router_step`](#add_router_step) or [`add_code_step`](#add_code_step).
  </Step>

  <Step title="Fix issues as you go">
    Every edit returns the updated flow and the validator's issues. Fix the **blocking** issues before adding more steps. **Advisory** issues never block the flow.
  </Step>

  <Step title="Test and validate">
    [`test_step`](#test_step) shows what a step actually returns, so later steps bind to real fields. Call [`validate_flow`](#validate_flow) before you call the build finished.
  </Step>
</Steps>

### Conventions shared by every tool

* **`flow_id` or `agent_id`, never both.** Every tool that works on an existing flow accepts either one, so an agent built in another session or in the UI can be addressed by its agent id alone. The flow must belong to your organization.
* **Bindings.** Reference earlier data as `{{step_name.path}}`. Paths can be nested, for example `{{step_1.data.items[0].id}}`. Trigger fields are `{{trigger.path}}`. Only bind to fields the source step's schema declares, or that a `test_step` sample showed.
* **Connections are bound for you.** When the project has exactly one connection for a piece, `set_trigger` and `add_step` bind it automatically. When there are several, the response carries a `connection-candidates` advisory listing their `externalId`s. Pass one as `connection_id`. Never put a connection inside `input`.
* **Placing steps.** `parent_step` is the step to insert after; use the trigger's name (usually `trigger`) to insert first. `position` is `AFTER` (default), `INSIDE_LOOP` or `INSIDE_BRANCH`. `INSIDE_BRANCH` also needs a zero-based `branch_index`.

### Pieces you can build with

There is no tool that lists pieces, so this is the full catalog. `get_piece_schema` lists each piece's full set of actions and triggers.

| Piece | Use it for |
| - | - |
| `schedule` | Triggers only: `every_x_minutes`, `every_hour`, `every_day`, `every_week`, `every_month`, `cron_expression`. |
| `slack` | Notifications: `send_channel_message`, `send_direct_message`, `updateMessage`, `slack-find-user-by-email`, `request_approval_message`. |
| `salesforce` | `run_query` runs a SOQL query. |
| `hubspot` | `create-note`, `create-contact`, `update-contact`. `custom_api_call` calls the HubSpot API directly for objects with no dedicated action. |
| `attention` (actions) | `listConversationsV2`, `showConversation`, `updateConversation`, `listTeams`, `fetchTeamMembers`, `listUsers`, `createSnippet`, `askAttention`, `askAttentionV2`, `createDeck`, `listEmails`, `scorecardSummary`, `execute_agent_task`, `importConversation`, `listCalendarEvents`, `triggerNativeWorkflow`, `changeConversationOpportunity`, `updateTeamOpportunity`, `changeConversationLabels`, `grantConversationAccess`, `upsertAIScore`, `upsertDealTLDR`, `upsertDealRisk`, `custom_api_call`. |
| `attention` (triggers) | `webhookTrigger` (fires when a conversation finishes analysis), `giCalculatedTrigger`, `exportCRMFieldsTrigger`, `labelsChangedTrigger`, `userChangesCrmAssociationTrigger`, `newEmailImported`. See [Attention Triggers](/builder-101/triggers/attention-triggers). |

Some actions and triggers carry **Guidance** in their `get_piece_schema` entry: a recipe built from earlier builds, such as a worked cron expression with its timezone. Follow it when configuring that step.

## Create and read

### create\_agent\_flow

<Warning>Scope: `mcp:write`. Requires admin or Product role.</Warning>

Create a new agent in your organization, along with the flow it is built from. Start here: no other tool hands out a flow id. The new flow has no trigger and no steps.

<ParamField query="display_name" type="string" required>
  Name of the agent as people will see it in Attention, e.g. `Deal desk notifier`.
</ParamField>

<ParamField query="description" type="string">
  What the agent is for, in one or two sentences. Shown alongside the agent in Attention.
</ParamField>

<ParamField query="folder_name" type="string">
  Agent folder to file the agent in, matched exactly but ignoring case. If no folder has that name, nothing is created and the response lists the folders that exist. Omit to create the agent outside any folder.
</ParamField>

<ParamField query="create_folder" type="boolean">
  Create the folder named by `folder_name` if it doesn't exist yet. A folder that already has the name is used either way.
</ParamField>

**Returns:** `agent_id`, `flow_id`, `created`, the folder used (`folder_id`, `folder_name`, `folder_created`) and any `warnings`.

***

### get\_flow

<Note>Scope: `mcp:read`. Requires admin or Product role.</Note>

Read a flow: its trigger, its steps with their configured inputs, and the validator's current issues. Call it before editing a flow you didn't just build.

<ParamField query="flow_id" type="string">
  ID of the flow to read. Pass this or `agent_id`.
</ParamField>

<ParamField query="agent_id" type="string">
  ID of the agent whose flow to read. Pass this or `flow_id`.
</ParamField>

**Returns:** The flow's trigger and steps, plus blocking and advisory issues.

***

### get\_piece\_schema

<Note>Scope: `mcp:read`. Requires admin or Product role.</Note>

Look up a piece so triggers and steps can be written with exact ids. With `piece_name` alone, it returns an index of the piece's action and trigger ids (up to 60 of each, no input schemas). With `action_name` or `trigger_name`, it returns that entry's input properties (exact key, type, whether required, static dropdown values), its declared output keys and any Guidance.

<ParamField query="piece_name" type="string" required>
  Piece to look up, e.g. `slack`.
</ParamField>

<ParamField query="action_name" type="string">
  Return this one action's inputs and output. Must be an id from the index.
</ParamField>

<ParamField query="trigger_name" type="string">
  Return this one trigger's inputs and output. Must be an id from the index.
</ParamField>

**Returns:** The piece index, or one action's or trigger's full schema.

<Tip>
  It doesn't resolve dropdowns whose values come from the connected account, such as Slack channels or CRM pipelines. Use [`get_action_options`](#get_action_options) for those.
</Tip>

***

### get\_action\_options

<Warning>Scope: `mcp:write`. Requires admin or Product role.</Warning>

Resolve what a dropdown property actually offers, the same way the builder does when someone opens it. This runs the piece's options code against the connected account, which is why it needs the write scope. It only reads.

<ParamField query="piece_name" type="string" required>
  Piece the entry belongs to, e.g. `slack`.
</ParamField>

<ParamField query="property_name" type="string" required>
  Exact property key from `get_piece_schema`, e.g. `channel`.
</ParamField>

<ParamField query="action_name" type="string">
  Action whose property to resolve. Pass this or `trigger_name`.
</ParamField>

<ParamField query="trigger_name" type="string">
  Trigger whose property to resolve. Pass this or `action_name`.
</ParamField>

<ParamField query="connection_id" type="string">
  `externalId` of the connection to resolve against, as `list_connections` reports it. Required for values that come from the connected account; without it the dropdown comes back disabled.
</ParamField>

<ParamField query="input" type="object">
  Values already chosen for the entry's other properties, keyed by property name. A dropdown that depends on a sibling property resolves to nothing without it.
</ParamField>

<ParamField query="flow_id" type="string">
  Resolve in the context of this flow, so the property can see the flow's saved step samples. Pass this, `agent_id`, or neither.
</ParamField>

<ParamField query="agent_id" type="string">
  Same as `flow_id`, naming the agent instead.
</ParamField>

<ParamField query="search" type="string">
  Case-insensitive filter over the returned labels and values.
</ParamField>

**Returns:** Up to 50 options, each with label and value.

## Edit the flow

`set_trigger`, `add_step`, `add_loop_step`, `add_router_step`, `add_code_step`, `update_step` and `delete_step` all return the flow's new contents and the validator's issues in the same response. `update_step`, `delete_step` and `set_trigger` overwrite what was there, and there is no undo.

### set\_trigger

<Warning>Scope: `mcp:write`. Requires admin or Product role. **Destructive**: replaces the current trigger.</Warning>

Set what fires the flow, replacing the current trigger. The trigger keeps its step name, so `{{trigger.field}}` bindings still resolve, but they're re-checked against the new trigger's output. This can't clear a trigger, and only piece triggers are supported.

<ParamField query="flow_id" type="string">
  ID of the flow. Pass this or `agent_id`.
</ParamField>

<ParamField query="agent_id" type="string">
  ID of the agent. Pass this or `flow_id`.
</ParamField>

<ParamField query="piece_name" type="string" required>
  Piece the trigger belongs to, e.g. `schedule`.
</ParamField>

<ParamField query="trigger_name" type="string" required>
  Exact trigger id from `get_piece_schema`, e.g. `every_x_minutes`.
</ParamField>

<ParamField query="input" type="object">
  Trigger inputs keyed by property name. Replaces the whole input; omitting it sets the trigger with no inputs.
</ParamField>

<ParamField query="display_name" type="string">
  Label shown in the builder. An existing label is kept when omitted.
</ParamField>

<ParamField query="connection_id" type="string">
  `externalId` of the connection to run on, from a `connection-candidates` advisory. Because the trigger is replaced whole, send `piece_name`, `trigger_name` and the current `input` in the same call.
</ParamField>

<Tip>
  For a `schedule` trigger, copy the cron format and timezone from the trigger's Guidance. A schedule that never comes due raises no issue. It just looks like a flow that hasn't fired yet.
</Tip>

***

### add\_step

<Warning>Scope: `mcp:write`. Requires admin or Product role.</Warning>

Insert a piece action into the flow. For loops, routers and code, use the dedicated tools below.

<ParamField query="flow_id" type="string">
  ID of the flow. Pass this or `agent_id`.
</ParamField>

<ParamField query="agent_id" type="string">
  ID of the agent. Pass this or `flow_id`.
</ParamField>

<ParamField query="step_name" type="string" required>
  Unique machine name, e.g. `step_3`. Letters, digits and underscores only. Later steps reference it as `{{step_name.path}}`.
</ParamField>

<ParamField query="piece_name" type="string" required>
  Piece the action belongs to, e.g. `slack`.
</ParamField>

<ParamField query="action_name" type="string" required>
  Exact action id from `get_piece_schema`, e.g. `send_channel_message`.
</ParamField>

<ParamField query="parent_step" type="string" required>
  Existing step to insert after. Use the trigger's name (usually `trigger`) to insert first.
</ParamField>

<ParamField query="input" type="object">
  Action inputs keyed by property name, e.g. `{"text": "{{step_1.body.summary}}"}`.
</ParamField>

<ParamField query="display_name" type="string">
  Label shown in the builder. Defaults to the action name.
</ParamField>

<ParamField query="position" type="string">
  `AFTER` (default), `INSIDE_LOOP` or `INSIDE_BRANCH`.
</ParamField>

<ParamField query="branch_index" type="integer">
  Zero-based branch of the parent router. Required with `INSIDE_BRANCH`.
</ParamField>

<ParamField query="connection_id" type="string">
  `externalId` of the connection to run on. Omit it to bind the project's only connection for the piece.
</ParamField>

***

### add\_loop\_step

<Warning>Scope: `mcp:write`. Requires admin or Product role.</Warning>

Add a loop that runs its body once per element of a list. The loop is created empty. Add its body with further calls using `position: INSIDE_LOOP` and `parent_step` set to the loop's `step_name`. Inside the body, read the current element as `{{step_name.item}}` and its position as `{{step_name.index}}`. `{{step_name.iterations}}` exists only after the loop finishes.

<ParamField query="flow_id" type="string">
  ID of the flow. Pass this or `agent_id`.
</ParamField>

<ParamField query="agent_id" type="string">
  ID of the agent. Pass this or `flow_id`.
</ParamField>

<ParamField query="step_name" type="string" required>
  Unique machine name, e.g. `for_each_row`.
</ParamField>

<ParamField query="parent_step" type="string" required>
  Existing step to insert after.
</ParamField>

<ParamField query="items" type="string" required>
  A binding onto a list an earlier step produced, e.g. `{{step_1.body.results}}`. A reference, not the list itself.
</ParamField>

<ParamField query="display_name" type="string">
  Label shown in the builder.
</ParamField>

<ParamField query="position" type="string">
  `AFTER` (default), `INSIDE_LOOP` or `INSIDE_BRANCH`.
</ParamField>

<ParamField query="branch_index" type="integer">
  Zero-based branch of the parent router. Required with `INSIDE_BRANCH`.
</ParamField>

***

### add\_router\_step

<Warning>Scope: `mcp:write`. Requires admin or Product role.</Warning>

Add a router that sends execution down one of several branches. Every branch starts empty. Fill one with further calls using `position: INSIDE_BRANCH`, `parent_step` set to the router's `step_name`, and its `branch_index`. Branch conditions can't be changed after the router is created.

<ParamField query="flow_id" type="string">
  ID of the flow. Pass this or `agent_id`.
</ParamField>

<ParamField query="agent_id" type="string">
  ID of the agent. Pass this or `flow_id`.
</ParamField>

<ParamField query="step_name" type="string" required>
  Unique machine name, e.g. `route_by_status`.
</ParamField>

<ParamField query="parent_step" type="string" required>
  Existing step to insert after.
</ParamField>

<ParamField query="branches" type="array" required>
  The branches, in order. At least one is required.

  <Expandable title="branch fields">
    <ParamField query="branch_name" type="string" required>
      Label shown in the builder.
    </ParamField>

    <ParamField query="branch_type" type="string">
      `CONDITION` (default) runs when its conditions match. `FALLBACK` runs only when no condition branch matched. At most one branch may be a fallback. Put the catch-all here, not in a condition.
    </ParamField>

    <ParamField query="conditions" type="array">
      A list of AND groups: conditions within a group are ANDed, groups are ORed. Each condition has `first_value` (usually a binding), `operator` (e.g. `TEXT_EXACTLY_MATCHES`, `TEXT_CONTAINS`, `NUMBER_IS_GREATER_THAN`, `BOOLEAN_IS_TRUE`, `EXISTS`), `second_value`, and `case_sensitive` (text operators only). Omit `second_value` for `BOOLEAN_IS_TRUE`, `BOOLEAN_IS_FALSE`, `EXISTS`, `DOES_NOT_EXIST`, `LIST_IS_EMPTY` and `LIST_IS_NOT_EMPTY`. Leave `conditions` out on a fallback branch.
    </ParamField>
  </Expandable>
</ParamField>

<ParamField query="execution_type" type="string">
  `EXECUTE_FIRST_MATCH` (default) runs only the first matching branch. `EXECUTE_ALL_MATCH` runs every matching branch.
</ParamField>

<ParamField query="display_name" type="string">
  Label shown in the builder.
</ParamField>

<ParamField query="position" type="string">
  `AFTER` (default), `INSIDE_LOOP` or `INSIDE_BRANCH`.
</ParamField>

<ParamField query="branch_index" type="integer">
  Zero-based branch of the *parent* router. Required with `INSIDE_BRANCH`.
</ParamField>

```json Example branches theme={null}
[
  {
    "branch_name": "is open",
    "branch_type": "CONDITION",
    "conditions": [[{ "first_value": "{{trigger.status}}", "operator": "TEXT_EXACTLY_MATCHES", "second_value": "open" }]]
  },
  { "branch_name": "everything else", "branch_type": "FALLBACK" }
]
```

***

### add\_code\_step

<Warning>Scope: `mcp:write`. Requires admin or Product role.</Warning>

Add a JavaScript step for reshaping that no piece does, such as reformatting a payload, filtering a list or computing a value. Prefer a piece action wherever one exists: a piece declares its inputs and outputs, and code declares nothing. The source is parsed when it's sent, and code that doesn't parse is refused with each error's line and column.

<Warning>
  A code step's `input` must never reference a connection. Inputs reach the code already resolved, so a connection there would hand the credential to the code. Read the connected system in a piece step and pass its output in.
</Warning>

<ParamField query="flow_id" type="string">
  ID of the flow. Pass this or `agent_id`.
</ParamField>

<ParamField query="agent_id" type="string">
  ID of the agent. Pass this or `flow_id`.
</ParamField>

<ParamField query="step_name" type="string" required>
  Unique machine name, e.g. `build_payload`.
</ParamField>

<ParamField query="parent_step" type="string" required>
  Existing step to insert after.
</ParamField>

<ParamField query="code" type="string" required>
  A module exporting one async function that takes the inputs object, e.g. `export const code = async ({ rows }) => rows.length;`
</ParamField>

<ParamField query="input" type="object">
  Values passed to the function, keyed by the parameter names it destructures, e.g. `{"rows": "{{step_1.body.results}}"}`.
</ParamField>

<ParamField query="package_json" type="string">
  npm manifest as JSON, e.g. `{"dependencies":{"dayjs":"1.11.10"}}`. Omit when there are no dependencies.
</ParamField>

<ParamField query="display_name" type="string">
  Label shown in the builder.
</ParamField>

<ParamField query="position" type="string">
  `AFTER` (default), `INSIDE_LOOP` or `INSIDE_BRANCH`.
</ParamField>

<ParamField query="branch_index" type="integer">
  Zero-based branch of the parent router. Required with `INSIDE_BRANCH`.
</ParamField>

***

### update\_step

<Warning>Scope: `mcp:write`. Requires admin or Product role. **Destructive**: overwritten values are not recoverable.</Warning>

Change an existing step. Send only what changes. The step keeps its name, so bindings that point at it still resolve.

<ParamField query="flow_id" type="string">
  ID of the flow. Pass this or `agent_id`.
</ParamField>

<ParamField query="agent_id" type="string">
  ID of the agent. Pass this or `flow_id`.
</ParamField>

<ParamField query="step_name" type="string" required>
  Step to change.
</ParamField>

<ParamField query="input" type="object">
  Replaces the step's **whole** input, so include every property the step needs. Omit to keep the current input.
</ParamField>

<ParamField query="display_name" type="string">
  New label shown in the builder.
</ParamField>

<ParamField query="piece_name" type="string">
  Switch to a different piece. Must be sent with `action_name`.
</ParamField>

<ParamField query="action_name" type="string">
  Switch to a different action. Must be sent with `piece_name`.
</ParamField>

<ParamField query="connection_id" type="string">
  Rebind the step to this connection. Sent alone, it changes only the connection.
</ParamField>

<ParamField query="source_code" type="string">
  Code steps only: the complete replacement module. This is the only way to change what a code step runs.
</ParamField>

<ParamField query="package_json" type="string">
  Code steps only, sent with `source_code`. Re-send the current manifest to keep dependencies; it's cleared otherwise.
</ParamField>

***

### delete\_step

<Warning>Scope: `mcp:write`. Requires admin or Product role. **Destructive**: there is no undo.</Warning>

Remove a step. Deleting a loop or router deletes everything nested inside it. Bindings aren't rewritten: steps that read the deleted step come back as blocking `binding` issues to repoint with `update_step`. The trigger can't be deleted; replace it with `set_trigger`.

<ParamField query="flow_id" type="string">
  ID of the flow. Pass this or `agent_id`.
</ParamField>

<ParamField query="agent_id" type="string">
  ID of the agent. Pass this or `flow_id`.
</ParamField>

<ParamField query="step_name" type="string" required>
  Step to remove, exactly as `get_flow` reports it.
</ParamField>

## Verify

### test\_step

<Warning>Scope: `mcp:write`. Requires admin or Product role.</Warning>

Run one action step and show what it returned, so later steps bind to fields the step really produces.

* An action the piece marks as a **read** runs for real against the connected system. The result is shown and saved as the step's sample.
* An action marked as a **write**, or not marked, is stubbed unless the piece ships its own test handler. This tool never asks the builder to perform a write.
* A **code step** is compiled and checked for side effects, and each finding is reported with its location. It is not executed.

The trigger can't be tested. Samples are shown up to 4,000 characters, with field names listed separately. Each organization shares a budget of about 30 step tests per minute. A timeout doesn't mean the step didn't run: check `get_flow` for a saved sample before retrying.

<ParamField query="flow_id" type="string">
  ID of the flow. Pass this or `agent_id`.
</ParamField>

<ParamField query="agent_id" type="string">
  ID of the agent. Pass this or `flow_id`.
</ParamField>

<ParamField query="step_name" type="string" required>
  Action step to test, exactly as `get_flow` reports it.
</ParamField>

<ParamField query="dry_run" type="boolean">
  Stub the step instead of running it, even a read. Nothing reaches the connected system and no sample is saved. Default: `false`.
</ParamField>

***

### validate\_flow

<Note>Scope: `mcp:read`. Requires admin or Product role.</Note>

Run the validator over the flow without changing it. Call it before you call a build finished: fix every blocking issue, then validate again. It returns the verdict only; read the flow itself with `get_flow`.

<ParamField query="flow_id" type="string">
  ID of the flow. Pass this or `agent_id`.
</ParamField>

<ParamField query="agent_id" type="string">
  ID of the agent. Pass this or `flow_id`.
</ParamField>

**Returns:** Blocking and advisory issues, plus each step's output status.

| Advisory | Meaning |
| - | - |
| `unverified` | The binding's shape couldn't be checked. |
| `conditional` | Reads a step inside a router branch or loop that may not have run. |
| `skipped` | The validator didn't check that step. |
| `expression` | An input computes a value inline instead of naming one earlier field. |
| `type` | A binding's type doesn't fit the field it feeds. |
| `code` | A code step whose output isn't known. |
| `connection-candidates` | Lists the connections a step could bind to. |

| Output status | Meaning |
| - | - |
| `declared` | An authoritative shape from the piece or Attention's own schema. |
| `sampled` | The example output the piece ships with. |
| `overlay` | A transcribed schema of a third-party API. |
| `captured` | Taken from a real `test_step` run. |
| `unknown` | Nothing has run and nothing declared a shape. Normal for code steps. |

A blocking `connection` issue means a step has no connection bound. If the project has a connection for that piece, bind it with `connection_id`. If it has none, add one in the Attention UI, or with `create_connection` for the pieces it supports.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.