Skip to main content

Workflow: Agent Type

Introduction​

An Agent workflow wraps an LLM agent instead of a fixed sequence of steps. Where a standard workflow always runs the same activities in the same order, an Agent workflow gives a model a system prompt, a set of other workflows it can call as tools, and a goal — the model decides what to call and in what order, then reports back through a structured result.

Use workflowType: Agent when the task needs judgment over ambiguous or unstructured input: triage, summarization, exception handling, freeform Q&A. If the same inputs should always produce the same steps, use a standard workflow instead — Agent workflows cannot have activities; everything they do comes from agent.instructions, agent.tools, and the model's own reasoning.

An Agent workflow runs one of two kinds of session:

  • task — the agent works until it calls the built-in set_result tool, which ends the session and returns a result, transcript, and sessionId. Every session started by a direct invocation — a trigger, a schedule, Workflow/Execute@1, the workflow execution API, or another workflow's tools[]/agents[] — runs as task, including an agent whose YAML declares session.type: chat.
  • chat — an open-ended, multi-turn conversation with no set_result and no automatic end: an assistant turn that calls no tool is the answer. Every session started over the Agent Responses API runs as chat — including an agent whose YAML declares session.type: task.

In other words, the caller decides which kind of session actually runs, not agent.session.type: a workflow-triggered or tool-invoked run is always task; a Responses API conversation is always chat. agent.session.type is still validated (see AGT-005) and documents your intent, but it has no effect on which loop executes today.

YAML Structure​

workflow:
name: "Agents / Order Exception Triage"
workflowId: "6f1c2c1e-1b2a-4d3e-9f00-000000000001"
workflowType: "Agent"
executionMode: "Sync" # required
isActive: true

agent:
description: "Investigates a delayed order and proposes an action"
instructions: |
You are an operations analyst. Load the order first, then reason
about the delay, then call set_result with an action and a reason.
model:
fromConfig: "ai.default" # org config name; see Model Configuration below
name: "claude-opus-5" # optional override of the config's model
temperature: 0.2 # optional
session:
type: "task" # "task" | "chat"
maxTurns: 25 # default 20, max 100
timeout: 300 # seconds, default 300
result: # optional JSON Schema for set_result's argument
type: object
properties:
action: { type: string }
reason: { type: string }
required: [action, reason]
skills: ["shipping-terms", "exception-playbook"] # accepted now; runtime ships later
tools:
- workflow: "MCP / Get Order Status" # workflow name or workflowId
instructions: "Call first to load the order before reasoning."
- workflow: "Notifications / Notify Ops"
instructions: "Only after you have decided on an action."
mcp: # accepted now; runtime ships later
- fromConfig: "mcp.carrier-portal"
agents: # accepted now; runtime ships later
- agent: "Agents / Rate Lookup"
modes: ["task", "chat"]
- url: "https://partner.example.com/.well-known/agent.json"
modes: ["task"]

inputs:
- name: "orderNumber"
type: "string"
props: { required: true, description: "Order to triage" }

# outputs: not needed here — result, transcript, and sessionId are always produced

activities is not allowed on an Agent workflow — an activities: list next to agent: fails validation (see AGT-004). triggers and schedules are declared exactly as on any other workflow, and trigger variables such as entity and eventType are available to the instructions template — but see Triggers below for what running one synchronously means for an Agent workflow.

The agent Section​

Every property below lives under the top-level agent: key. agent itself has no additional properties beyond these.

PropertyTypeRequiredDefaultDescription
descriptionstringNo—What this agent does. Shown to callers, and used as the tool description when another agent lists this workflow under its own tools[].
instructionsstringYes (minLength: 1)—System instructions for the model. A Handlebars template evaluated over workflow variables and inputs, rendered once at session start.
modelobjectNo—Model selection. No properties beyond the three below.
model.fromConfigstringNoai.defaultOrganization config name holding provider, model, apiKey, endpoint.
model.namestringNo—Overrides the config's model name for this workflow only.
model.temperaturenumber (0–2)No—Sampling temperature.
model.contextWindowintegerNo—The model's context window in tokens, used to decide when to compress history. Set this when model.name overrides the config's model — the org config's own window describes its own model, not your override. See Model Configuration for the full resolution order.
sessionobjectNo—Session behavior. No properties beyond the three below.
session.typetask | chatNotaskDocuments intent only — see Introduction. The invoking surface (workflow vs. the Responses API), not this field, decides which kind of session actually runs.
session.maxTurnsinteger (1–100)No20Maximum agent turns before the session is force-ended.
session.timeoutinteger (>= 1)No300Session timeout, in seconds.
resultobject (JSON Schema, type required, must be object)No—The schema the set_result tool's argument must satisfy. Shapes the result output.
skillsarray of stringsNo—Installed skill names to enable. Accepted and validated now; runtime support ships in a later release.
toolsarray of objectsNo—Other workflows exposed to the agent as callable tools. See Tools.
tools[].workflowstring (minLength: 1)Exactly one of workflow/builtin, per entry—Workflow name or workflowId to expose as a tool.
tools[].builtinstringExactly one of workflow/builtin, per entry—A server-provided tool name, such as file.create — no workflow needed. See Built-in Tools.
tools[].instructionsstringNo—When and how the agent should use this tool; folded into the tool's description.
tools[].modeauto | approvalNoautoauto runs the tool the moment the model calls it. approval pauses the call for a person to approve or decline first. See Tool Approval.
mcparray of objectsNo—Outbound MCP server connections. Accepted and validated now; runtime support ships in a later release.
mcp[].fromConfigstringYes, per entry—Organization config name for the MCP connection.
agentsarray of objectsNo—Allow list of other agents this agent may invoke. Accepted and validated now; runtime support ships in a later release.
agents[].agentstringExactly one of agent/url, per entry—Name of another Agent workflow in this organization.
agents[].urlstring (format: uri)Exactly one of agent/url, per entry—Remote A2A agent card URL.
agents[].modesarray of task | chatNo—Which session modes this agent may be invoked in.

Inputs and the First Message​

inputs: on an Agent workflow serves the same two roles it serves on an McpTool workflow:

  1. Direct invocation — when the workflow runs directly (API, schedule, entity trigger, or another workflow's Workflow/Execute@1), the input values are rendered into the agent's first user-turn message alongside anything the instructions template references.
  2. Invocation as a tool — when this workflow appears in another agent's tools[] (or agents[]), inputs (name, type, props.required, props.description) becomes the JSON Schema tool-call signature the calling model sees. Keep props.description populated — it's the parameter description the calling model reads when deciding how to fill the call.

A reserved input, __session, does not need to be declared under inputs: — see Session Limits and the __session Override.

Tools​

Each entry in agent.tools[] points at another workflow by workflow (name or workflowId) and turns it into a callable tool for the agent, alongside the built-in set_result tool described below. An entry can instead name a builtin tool that needs no workflow at all — see Built-in Tools.

  • Name. The tool's name is the target workflow's display name, sanitized to match ^[a-zA-Z0-9_-]{1,64}$: any character outside that set (spaces, /, punctuation) becomes _. If two tools sanitize to the same name, later collisions get a numeric suffix — the second becomes _2, the third _3 — applied within the 64-character limit, shortening the base name to make room if needed. Keep the workflows you expose to the same agent short and distinguishable after this substitution.
  • Description. Built from the target workflow's own description (or its agent.description, if the target is itself an Agent workflow), plus this entry's instructions — use instructions to say when the agent should reach for this tool and what to expect back.
  • Schema. The tool's argument schema is the target workflow's inputs, exactly as for an McpTool workflow, with one exception: a type: file input is an integer file id for an agent and a URI for MCP (see Passing a chat file to a workflow tool).
  • Response. Calling the tool runs the target workflow synchronously. On success, the model receives the target's response output if it declares one, or the full outputs dictionary otherwise. If the target workflow fails, or its arguments don't bind against the schema, the model receives a JSON object { "error": "<message>" } as the tool result instead — the agent's session is not failed, so the model can see the error and react or retry.
  • Permissions. A tool whose workflow declares workflow.permission is left out of the session for a user without that permission, so the model never sees or offers it. A refused call returns { "error": { "code": "permission_denied", "message": "You do not have permission to run '<name>'." } }.
  • Existence is checked at run time, not save time. Saving or validating the YAML only checks that tools[].workflow is present (see AGT-007); resolving the name or ID against an actual, active workflow happens when the agent calls the tool. A tool that references a workflow which doesn't exist yet — or is later deleted — passes validation and only surfaces as a run-time error result the model sees.

Built-in Tools​

A tools[] entry can enable a tool the server provides directly, with no workflow behind it — set builtin instead of workflow:

agent:
tools:
- builtin: file.create
- workflow: "Orders / Get Status"

An entry has exactly one of workflow or builtin. instructions and mode work the same way as for a workflow tool — instructions is folded into the tool's description, and mode (default auto) gates it behind approval the same way (see Tool Approval). A built-in runs as the session user — the chat user, or the user who started a task run — so row-level security applies the same way it does in the UI.

CXTMS also ships three read-only data tools (data.query, data.schema, data.type) that let an agent query the organization's own GraphQL data under the same guards described below for file.create's query mode. This page covers file.create, the built-in that lets an agent hand back a file.

file.create​

Writes a PDF, Word (.docx), Excel (.xlsx) or CSV file and attaches it to the session — the model sees the tool as file_create, and the file's content never goes back to the model; the tool result only names it.

ArgumentTypeMeaning
formatpdf | docx | xlsx | csvRequired.
fileNamestringRequired. The name without an extension — it's sanitized, and the extension comes from format.
markdownstringFor pdf/docx: the document body. A Markdown subset (pipe tables, autolinks, strikethrough/emphasis); raw HTML stays literal text and images are dropped to their alt text, so rendering never fetches a remote URL.
titlestringOptional, pdf/docx: the header title, at most 200 characters. Defaults to fileName.
rowsarray of objectsFor xlsx/csv: one object per row; its keys become the columns.
querystringFor xlsx/csv, instead of rows: a GraphQL query the server runs and pages through, under the same read-only, organization-scoped guards as the data.query built-in. It must declare $skip: Int and $take: Int and pass them to the list field — the server pages through the results itself.
variablesobjectOptional, with query.
itemsPathstringRequired with query: the path to the list in the result, e.g. orders.items.
columnsarray of { field, label? }Optional, rows/query: the column order and headings; field may be a dot path into a nested object. Omit it to use every property found on the rows, in encounter order.

Pass exactly one of markdown, rows or query, matching the format. A successful call returns { fileName, attachmentId, format, size, rows?, truncated? } — rows/truncated only for xlsx/csv.

agent:
tools:
- builtin: file.create
instructions: "Use xlsx for tabular exports the user can open in Excel; use pdf for a written summary."

Limits

LimitValue
Rendered file size25 MB
markdown length200,000 characters
rows / query row count50,000 — a query past this cap stops paging and reports truncated: true; rows over the cap is an error.
query total raw result size64 MB of JSON read across all pages — a query past this cap also stops paging and reports truncated: true, same as hitting the row cap.

Errors. Like the data tools, a refused or failed call never fails the session — the model gets a JSON error it can react to or retry, and only the streamed mcp_call item is marked failed:

Casecode
Unknown format, missing fileName, title over 200 characters, none or more than one of markdown/rows/query, rows not an array, query missing itemsPath, $skip or $take, or itemsPath not pointing at a listinvalid_arguments
markdown, rows, a query page, or the rendered file over its limittoo_large
Rendering the PDF, Word document or spreadsheet failedrender_failed
Saving the file failed, or the query itself failedfailed
query refused by the data guards (a mutation, an unscoped or restricted field, wrong organization, too deep)the matching data.query code (read_only, organization_scope, restricted, depth_exceeded, syntax)

Barcodes​

In pdf and docx documents, a Markdown image whose URL starts with barcode: is drawn as a barcode on the server (nothing is fetched). It works in paragraphs, list items and table cells. Barcodes are not supported in xlsx/csv.

![ORD-1001](barcode:code128/ORD-1001)
![P-01](barcode:qr/P-01?width=1in)
![PLT-77](barcode:pdf417/PLT-77?width=3in&height=1in)
| Order | Label |
|---|---|
| ORD-1001 | ![ORD-1001](barcode:code128/ORD-1001?width=50mm&text=false) |
  • Address: barcode:<format>/<value>, with the value URL-encoded. The alt text becomes the image's alternative text.
  • Formats (case-insensitive; ZXing enum names such as CODE_128 also work):
    • linear: code128, code39, code93, codabar, itf, msi, plessey, ean13, ean8, upca, upce;
    • 2D: qr, datamatrix, aztec, pdf417.
  • Options (all optional):
    • width and height, as a number plus in, mm or cm, each 5–200 mm;
    • text=false hides the value printed under linear codes (2D codes never print it).

Sizes:

KindDefaultOnly one dimension givenBoth given
Linear2.5 in × 0.6 inthe other keeps the ratio (height = width × 0.24)used as given
QR, Data Matrix, Aztec1.2 in squarestays squaresquare, using the smaller
PDF4172.5 in × 1 inthe other keeps the ratio (height = width × 0.4)used as given
  • Every module (bar or cell) is at least 0.17 mm, so the code prints and scans.
  • When no width is given, a code too dense for the default width grows just wide enough, up to 200 mm.
  • An explicit size that is too small, or content needing more than 200 mm, is an error naming the minimum width.
  • In a PDF, a barcode wider than its table cell shrinks proportionally to fit.
  • QR, Data Matrix, Aztec and PDF417 encode UTF-8.
  • EAN-13, UPC-A and EAN-8 values given without their check digit print it.

Limits: a value can be at most 1,000 characters, and a document can hold at most 200 barcodes.

Errors: a bad barcode fails the call with invalid_arguments naming the problem, and no file is saved, so the model can fix it and retry. Examples:

  • unknown barcode format 'code11'
  • width '500mm' is larger than 200mm
  • ean13 cannot encode 'ABC'
  • code128 value '…' needs a width of at least 23mm

Producing Files​

An agent can hand the user a file instead of, or alongside, an answer in text, two ways:

  • file.create — the agent writes the file itself, from Markdown or from rows or a query result.
  • A workflow tool's own output — when a tool the agent calls returns a file (a Document workflow's rendered PDF, or a Utilities/Export file in the organization's export area), the runtime captures it as a produced file automatically; nothing is declared in YAML for this. The tool result the model sees has each file replaced by { fileName, attachmentId } in place of its bytes, so content never reaches the model either way.

Either way, the file becomes an Attachment linked to the agent session with origin Produced — distinct from a file a person uploaded — and shows up in three places:

  • In chat, as a card below the agent's reply. The Agent Responses API's internal route streams response.tms.file_produced as each file is recorded, so a client can offer the download immediately rather than waiting for the response to finish.
  • In the transcript, as an attachment block on the tool's row, carrying "origin": "produced" (see GraphQL: Agent Sessions).
  • In the AI Assistant's Library, listed as "Produced in …" alongside uploaded files (see AI Assistant).

A produced file's content is never sent back to the model on a later turn — only the tool result that named it stays in history. A task run also gets the files back directly: see the files output.

Tool Approval​

Set mode: approval on a tools[] entry to stop that tool from running the moment the model calls it — a person has to approve or decline the call first. mode: auto (the default, and the same as omitting mode) runs the tool immediately, as described above.

agent:
tools:
- workflow: "Orders / Get Status" # auto (default): runs immediately
- workflow: "Orders / Cancel Shipment"
mode: approval # waits for a person

What happens next depends on which kind of session is running (see Introduction):

  • In a chat session (the Agent Responses API), the call pauses instead of running: the response ends with an approval request item, and the session status becomes AwaitingApproval. The conversation resumes once every pending call has a decision — approved (the tool then runs) or declined (the model is told the person declined, and why). The full request/response shape, the pause/resume protocol, and how a client renders it live in Agent Responses API and GraphQL: Agent Sessions.
  • In a task session (run from a trigger, a schedule, or another workflow's tools[]) there is nobody to ask. The call is refused — the model receives "This tool requires human approval and cannot run in an unattended session." as the tool's error result — and the agent carries on; the session itself does not fail. Don't give a task-only agent an approval tool it actually needs to call to finish its job: it will never get approval.
  • Whoever starts or is otherwise allowed to see a chat session may decide its approvals — the same rule that governs who may continue the conversation. See Ownership and Sharing below.
  • The person who started a chat session gets a notification when it pauses for their approval — see Approval Notifications.

The set_result Tool and agent.result​

A task session gets a built-in set_result tool at run time — you never declare it under tools. Its argument schema is exactly agent.result (when omitted, { type: object }). Calling it ends the session and becomes the result output; it's the only way a task session (and therefore every workflow-triggered or tool-invoked agent run) finishes. A chat session has no set_result tool — an assistant turn with no tool calls is the answer instead, as described in Introduction.

At run time, only agent.result's top-level required list is enforced against the set_result call — a call missing one of those properties is returned to the model as an error result, and it may retry. Property types and any nested schema (properties, items, formats, and so on) are not validated in this release, so a call with a required property present but wrongly typed is still accepted.

Session Limits and the __session Override​

agent.session.maxTurns (default 20, max 100) and agent.session.timeout (default 300 seconds) bound how long a session can run before it's force-ended without a result.

A caller can override these two limits for a single invocation — without editing the YAML — by passing a reserved input named __session:

__session:
maxTurns: 40
timeout: 600
type: "task" # accepted but not yet used

__session is not a session id and does not resume or attach to a prior session — every run of an Agent workflow starts a brand-new session, whether or not __session is passed. It's an optional object of maxTurns/timeout overrides (type is also accepted on the object but not yet used by the runtime). __session does not need to be declared under inputs:, and it's stripped out before the remaining inputs are rendered into the agent's first message.

Outputs​

Outputs are fixed for Agent workflows. When the session completes by calling set_result, the engine always produces exactly these four, no matter what — or whether anything — is declared under outputs::

OutputDescription
resultThe argument passed to set_result, matching agent.result's schema.
transcriptThe full turn-by-turn conversation log: prompts, tool calls, tool results, and model responses.
sessionIdThe session identifier.
filesEvery file the session produced — via file.create or a captured workflow-tool file — as { attachmentId, fileName, contentType, size, url }. url is presigned for 24 hours (null if it couldn't be signed; the failure is logged and the task still completes). [], not omitted, when nothing was produced.

If a task session ends without calling set_result — it hits session.maxTurns, hits session.timeout, or stops responding with tool calls after a couple of nudges — the workflow fails instead of returning outputs: the engine raises an error naming the session id (for example, Agent session <sessionId> ended without a result: exhausted its turn budget (20)), and no result, transcript, sessionId or files output is produced. Use the session id from that error to look up the AgentSession record and inspect the transcript of the failed run.

An outputs: section is not needed for an Agent workflow, and the engine ignores it for this workflow type — it never consults outputs: to decide what to produce. You may still add one (for example, to rename an output for a caller that expects a different name), and the usual rule that every entry needs a mapping still applies to it, but it has no effect on what the Agent runtime actually returns.

Model Configuration​

agent.model.fromConfig (default ai.default) names an organization configuration record shaped:

{
"provider": "Anthropic",
"model": "claude-sonnet-4-5",
"apiKey": "<your-api-key>",
"endpoint": "https://api.anthropic.com",
"contextWindow": 200000,
"supportsFiles": true
}

provider is one of Anthropic, OpenAI, or AzureOpenAI. supportsFiles (optional) says whether the model reads PDFs and images attached in chat — see Files in Chat; when it is absent it defaults to true for Anthropic and OpenAI and to false otherwise. Set up ai.default once per organization — every Agent workflow that doesn't override model.name or model.temperature shares it — or add further named configs (ai.<name>) and point specific workflows at them with model.fromConfig.

If the organization has no matching config, the instance-level AI setting is used as a fallback. If neither is configured, the workflow fails when it tries to start a session rather than at save time.

Context window resolution. The window used for history compression is resolved in order: the agent's own model.contextWindow, else the organization config's contextWindow (only used when model.name does not override the config's model — the config's window describes its own model, not yours), else 256,000 tokens. If you set model.name, also set model.contextWindow: otherwise the runtime falls back to the 256,000-token default even for a model with a much smaller window, and history compression won't kick in until a call actually fails or runs slow from an oversized prompt.

History Compression​

A long-running chat can outgrow the model's context window. The engine compresses history automatically — you don't configure anything to enable it — whenever the previous call's reported input-plus-output tokens reach 80% of the resolved context window: older messages are replaced with a model-written summary, stored as a role: "summary" transcript message carrying summarizesThroughSequence, and the turn continues on the shortened history.

  • The summary call is charged against the session, but may use at most 50% of the remaining session time budget; if it fails, or runs out of that budget, the turn falls back to running on the full, uncompressed history rather than blocking the response.
  • A provider that reports no token usage for a call never triggers compression — there's nothing to compare against the 80% threshold.
  • The summary's own token usage is recorded in the transcript's usage list with "kind": "summary", and counted in the response's overall usage.
  • A response created over the internal Responses route streams response.tms.history_compressed with { "through_sequence": n } when a compression happens mid-response; see Agent Responses API.
  • Compression is invisible to the model's own reasoning — the summary stands in for what it replaces — but visible to a transcript viewer: render a summary message as a divider ("earlier messages summarized"), not as a chat bubble.

Files in Chat​

In a chat session, a person can attach files to a message — up to 5 per message, 25 MB each: PDFs, images (PNG, JPG), text files (TXT, CSV, JSON, MD) and spreadsheets (XLSX, XLS). Nothing in agent: YAML opts in; every chat agent accepts them.

KindWhat the model receives
PDFThe document: bytes up to 4 MB, above that a presigned URL (URLs only for Anthropic; other providers refuse a PDF over 4 MB)
ImageThe image: bytes up to 4 MB, above that a presigned URL
TextThe text, cut at 200,000 characters with a truncation note
Spreadsheet (XLSX, XLS)Named only, never read; no model file support needed
  • Each file follows a label such as [Attached file: bol.pdf (PDF, 1.2 MB), file id 41], so the model can refer to it by name or id, and file content is framed as data, not instructions — like a tool result.
  • Files stay in the conversation: later turns can still read them. When history is compressed, the summary names the files, but their content leaves the history.
  • A file that can't be read when a turn runs reaches the model as [File <name> could not be read.]; the turn continues.
  • Each upload becomes an Attachment linked to the session, visible only to people who can see the chat, and listed in the AI Assistant's Library.
  • When the model config's supportsFiles is false (see Model Configuration), PDFs and images are refused when attached; text files and spreadsheets are still accepted.
  • Task sessions never receive files.

Write agent.instructions for chat agents that will receive documents to say what to do with them — "When the user attaches a bill of lading, read the shipper, consignee and pallet count" — and to answer only from what the file shows. The request shape, limits and error messages are in Agent Responses API: Attaching files.

Passing a chat file to a workflow tool​

Declare the workflow input as type: file:

inputs:
- name: importedFileUrl
type: file
props: { required: true, description: "The manifest file" }

The agent sees it as an integer file id and passes the id of a file the user attached or that it created with file_create. Before the workflow runs, the server checks the file belongs to this chat and replaces the id with a presigned URL valid for one hour, so the workflow reads {{ importedFileUrl }} as a URL (for example as Utilities/Import@1 fileUrl). Any other value returns { "error": { "code": "file_not_in_chat", … } } listing the chat's files, and the workflow does not run. MCP callers pass a file URL directly.

Declare file inputs on the workflow tools the agent calls, not on the Agent workflow's own inputs:. A type: file input on an Agent workflow's own inputs reaches that agent as a presigned URL, not a file id, so the agent cannot hand it to its own file tools (they take a file id of its own chat).

Session Ownership​

Every agent session — task or chat — has an owner scope (User, Division, or Organization) that governs who may see it, continue it, decide its approvals, and watch it live over onAgentSessionEvent. A system administrator can always see every session.

Defaults at creation, and they are defaults only — nothing in the YAML sets them:

  • A chat session (the Responses API) starts owned by User — the person who started it.
  • A task session (run from a workflow, with no person present) starts owned by Organization.

Change a session's owner — for example, to share a chat with a division or the whole organization so a teammate can pick up a pending approval — with the setAgentSessionOwner GraphQL mutation. The full scope rules, who may change ownership, and what changes when they do live in GraphQL: Agent Sessions.

Triggers​

An Agent workflow can carry a triggers: entry (for example type: Entity) just like a standard workflow. When it fires, the agent session runs synchronously inside the saving request: the request that added, modified, or deleted the triggering entity waits for the full agent session — potentially several model round-trips — to finish before it can complete, for up to agent.session.timeout (default 300 seconds), and it holds the workflow lock for that entity/organization for the whole duration.

For a trigger-bound agent, either:

  • keep agent.session.timeout short and agent.session.maxTurns low, so a slow or looping agent can't stall the triggering request for long, or
  • keep the trigger workflow itself lightweight (no agent: section) and have it invoke the Agent workflow asynchronously via Workflow/Execute@1 with executionMode: Async, so the triggering request returns immediately and the agent runs out-of-band.

Validation Rules​

Agent workflows are validated at save time:

CodeRule
AGT_001agent section is required for workflowType: Agent
AGT_002agent.instructions is required and non-empty
AGT_003executionMode must be Sync
AGT_004activities is not allowed on an Agent workflow
AGT_005agent.session.type must be task or chat
AGT_006agent.session.maxTurns must be 1–100; timeout must be positive
AGT_007agent.tools[].workflow is required on every entry
AGT_008agent.result, when present, must be a JSON Schema with type: object
AGT_009agent.agents[] entries need exactly one of agent/url; modes values must be task or chat
AGT_010agent.tools[].mode, when present, must be auto or approval

See Workflow Validation Errors for full messages, causes, and solutions.

Examples​

Order exception triage (task session)​

The agent from YAML Structure above, invoked directly or as another workflow's tool. It loads an order, reasons about the delay, and finishes by calling set_result — a typical task session run from a trigger or another workflow.

Customer-facing assistant with a guarded tool (chat session)​

An agent meant to be talked to over the Agent Responses API, with one read-only tool on auto and one side-effecting tool gated behind approval:

workflow:
name: "Agents / Order Assistant"
workflowId: "6f1c2c1e-1b2a-4d3e-9f00-000000000002"
workflowType: "Agent"
executionMode: "Sync"
isActive: true

agent:
description: "Answers questions about freight orders and can cancel a shipment with approval"
instructions: |
You are a freight operations assistant talking to a customer service rep.
Look up order status before answering. Only call Cancel Shipment when the
rep explicitly asks you to cancel — and expect it to need approval.
model:
fromConfig: "ai.default"
session:
type: "chat" # documents intent; a Responses API conversation runs as chat regardless
tools:
- workflow: "Orders / Get Status"
instructions: "Call before answering any question about an order."
- workflow: "Orders / Cancel Shipment"
mode: approval
instructions: "Only after the rep asks to cancel. Requires approval."

A rep talks to this agent through POST /api/organizations/{organizationId}/ai/responses with model: "Agents_Order_Assistant". Asking it to cancel a shipment pauses the conversation at AwaitingApproval until someone approves or declines the call — see Agent Responses API.

Best Practices​

  • Gate side-effecting tools behind mode: approval. Anything that cancels, refunds, deletes, sends, or otherwise changes state outside the conversation belongs behind approval; keep read-only lookups on auto (the default) so the agent isn't stalled waiting on every call.
  • Don't hand a task-only agent a tool it must call to finish. A task session has nobody to approve a call — the tool is refused and the agent has to work around it. Reserve approval tools for agents a person will actually be driving through a chat session.
  • Set model.contextWindow whenever you set model.name. Otherwise the runtime assumes the 256,000-token default, and history compression won't trigger at the right point for a smaller model.
  • Set supportsFiles on the model config to match the model. A model without vision or PDF support should say "supportsFiles": false, so the chat refuses those files up front instead of the provider failing the turn — see Files in Chat.
  • Give agent.result a required list. It's the only part of the schema enforced at run time — a missing required property is returned to the model as a retryable error, so the agent can correct itself instead of returning half a result.
  • Write tools[].instructions for the calling model, not for a human reader. It's folded straight into the tool's description — say when to call it and what to expect back, not just what it does.
  • Keep trigger-bound agents short and bounded, or move them off the triggering request entirely — see Triggers. A slow agent on an entity trigger holds up the save and the workflow lock for the whole session timeout.
  • Treat every tool result as untrusted data, not instructions — a carrier portal reply, a document, or any other tool output can contain text that looks like instructions. This is exactly the scenario mode: approval protects against for anything with a real-world side effect: even if the model is steered by something it read, a side-effecting call still stops for a person.