# Actions

[Open in workspace →](/workspace/_/ai/workflows)

An action step performs a concrete operation without AI involvement. There are several kinds:

## Reply

Sends a message to the customer.

| Setting   | Purpose                                                                                                                                                                                   |
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Content   | The message text. Supports template variables. When left empty, the reply sends whatever is in `{{args.content}}`, which is typically the text the AI composed in the previous tool call. |
| Assistant | Which assistant's name and avatar appear on the message. Defaults to the workspace's primary assistant. Override this when a sub-workflow should reply as a different persona.            |

When a reply follows a prompt step, you usually leave Content empty so it sends whatever the AI wrote. When a reply is a fixed message (like a greeting at the start of a conversation), you write the content directly.

**Example:** A greeting reply at the start of a conversation:

> Hi `{{user.name}}`, welcome to `{{workspace.name}}`! Ask me anything and I'll do my best to help.

## Search

Searches your knowledge base for content relevant to a query. The results are stored and made available to the next step (usually a prompt step that uses them to compose an answer).

| Setting      | Purpose                                                                                           |
| ------------ | ------------------------------------------------------------------------------------------------- |
| Query        | What to search for. Usually set to `{{args.content}}` to search for whatever the customer said.   |
| Token budget | How much content to retrieve. A higher budget gives the AI more context but uses more processing. |
| Sources      | Optionally limit which knowledge sources to search, or exclude specific ones.                     |

**Example:** A customer asks "How do I reset my password?" The search step looks up your knowledge base for password reset instructions, then passes the results to a prompt step that composes the answer.

## Escalate

Hands the conversation off to a human operator. An event is logged on the conversation so your team can see why it was escalated.

**Example:** The AI determines it can't answer a question. It escalates with the note "Escalated to support" so a team member can pick it up in the inbox.

## Choice

Presents the customer with a set of clickable options (buttons). The workflow pauses until the customer picks one, sends a text message, or the choice times out.

| Setting    | Purpose                                                                                   |
| ---------- | ----------------------------------------------------------------------------------------- |
| Content    | The message shown above the options.                                                      |
| Options    | The choices to display, each with a label and a value. Can also be generated dynamically. |
| Timeout    | How long to wait (in seconds) before the choice expires.                                  |
| Skippable  | Whether the choice can be bypassed if the value is already known.                         |
| Next       | The step to run after the customer picks an option.                                       |
| On message | The step to run if the customer sends a text message instead of picking an option.        |
| On timeout | The step to run if the choice times out.                                                  |

**Example:** Your billing workflow serves customers who belong to multiple organizations. A choice step asks "Which organization's billing should I look up?" and shows the customer's organizations as buttons.

## User update

Saves information to the customer's profile, like their email address or name.

**Example:** After collecting a customer's email during an escalation flow, a user update step saves it to their profile so operators can follow up.

## Workflow

Jumps to another workflow. This is how you compose larger automations from smaller, reusable pieces. A workflow action can start immediately or wait for the next trigger.

| Setting  | Purpose                                                                                           |
| -------- | ------------------------------------------------------------------------------------------------- |
| Workflow | Which workflow to run. Can also be set to the **parent workflow** to return from a sub-workflow.  |
| Initial  | `start` runs the workflow immediately. `idle` waits for the next customer message before running. |

**Example:** When your main workflow needs to collect an email, it jumps to a "Collect email" sub-workflow. Once the email is collected, the sub-workflow jumps back to the parent.

## Code

Runs a JavaScript snippet in an isolated worker. The code receives the workflow `context` (workspace, user, organizations, conversation messages, trigger) and `args` from the previous step. Whatever object it returns gets merged into `args` for the next step.

All outbound HTTP requests are blocked unless you attach a [connector](/docs/ai/connectors/). Each connector grants access to a set of allowed URLs and injects the API secret automatically, so credentials never appear in your code. You can attach multiple connectors to a single code step.

| Setting    | Purpose                                                                                                                 |
| ---------- | ----------------------------------------------------------------------------------------------------------------------- |
| Source     | The JavaScript code to run. A code editor opens in a dialog.                                                            |
| Connectors | Which connectors this code is allowed to use for outbound requests. Without a connector, all `fetch` calls are blocked. |

The function signature:

```js
/**
 * @param {object} input
 * @param {object} input.context
 * @param {{ id: string, name: string }} input.context.workspace
 * @param {{ internalId: string, externalId: string | number | null, type: "anonymous" | "identified", session?: { verified: boolean | null }, email?: string, name: string | null, attrs?: Record<string, string | number | boolean | null | undefined> | null }} input.context.user
 * @param {Array<{ internalId: string, externalId: string | number, name: string, attrs?: Record<string, string | number | boolean | null | undefined> | null }>} input.context.organizations
 * @param {Array<{ role: "user" | "operator" | "assistant", active: boolean }>} input.context.participants
 * @param {Array<{ role: "user" | "operator" | "assistant", content: string, channel: { kind: "email" } | null, data?: { choice?: { value: string | number | boolean } } | null }>} input.context.messages oldest-first, capped at 20
 * @param {{ kind: "message", message: { content: string, data?: { choice?: { value: unknown } } | null, channel: { kind: "email" } | null } } | { kind: "timeout", messageId: string }} input.context.trigger what woke this workflow run
 * @param {Record<string, unknown>} input.args from the previous step
 * @returns {Promise<Record<string, unknown>>} merged into args for the next step
 */
export default async function ({ context, args }) {
  return {};
}
```

:::tip
Paste the JSDoc signature into the [TypeScript Playground](https://www.typescriptlang.org/play/?filetype=js) to get type-checking and autocomplete while writing your code.
:::

**Example:** A code step uses a Stripe connector to look up the customer's active subscriptions. The code calls `fetch("https://api.stripe.com/v1/subscriptions?customer=...")`, the connector injects the Stripe API key as a Bearer token, and the code returns `{ subscriptionSummary: "..." }`. A prompt step downstream reads that summary and explains it to the customer in plain language.

### Security

Code steps sit between the AI and your external systems. The AI chooses which tool to call and fills in the parameters, but a user could manipulate the conversation to influence those values. Treat everything in `args` the same way you'd treat user input.

**Verify identity in code, not in prompts.** Before calling an API on behalf of a user, check `context.user.type` and `context.user.session.verified` in your code. A prompt rule like "only do this for verified users" can be bypassed through prompt injection. A code-level check cannot. The built-in Stripe presets do this with an `assertVerifiedWebUser` guard that throws if the session isn't verified.

**Re-derive sensitive values from context.** If your code needs a customer ID to call an external API, read it from `context.user.attrs` or `context.organizations` rather than trusting `args.customerId` that the AI filled in. A user could steer the AI into supplying someone else's ID. The Stripe presets cross-check: if `args.customerId` is present but doesn't match the value in the user's attributes, the request is rejected.

**Return only what the next step needs.** If an API response contains sensitive fields (tokens, internal IDs, PII of other users), filter them out before returning. Whatever you return lands in `args` and becomes visible to the AI and to the user through the response.

**Gate sensitive tools behind branches.** Use a [branch step](/docs/ai/workflows/steps/#branch) with the **User is verified** condition before any code step that accesses personal data. The tool won't appear to the AI for unverified users, so you get a second check beyond the code-level guard.
