Skip to content

Actions

Open in workspace →

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

Sends a message to the customer.

SettingPurpose
ContentThe 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.
AssistantWhich 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.

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).

SettingPurpose
QueryWhat to search for. Usually set to {{args.content}} to search for whatever the customer said.
Token budgetHow much content to retrieve. A higher budget gives the AI more context but uses more processing.
SourcesOptionally 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.

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.

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.

SettingPurpose
ContentThe message shown above the options.
OptionsThe choices to display, each with a label and a value. Can also be generated dynamically.
TimeoutHow long to wait (in seconds) before the choice expires.
SkippableWhether the choice can be bypassed if the value is already known.
NextThe step to run after the customer picks an option.
On messageThe step to run if the customer sends a text message instead of picking an option.
On timeoutThe 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.

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.

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.

SettingPurpose
WorkflowWhich workflow to run. Can also be set to the parent workflow to return from a sub-workflow.
Initialstart 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.

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. 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.

SettingPurpose
SourceThe JavaScript code to run. A code editor opens in a dialog.
ConnectorsWhich connectors this code is allowed to use for outbound requests. Without a connector, all fetch calls are blocked.

The function signature:

/**
* @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 {};
}

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.

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 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.