Skip to main content
Every agent can turn a conversation like “every weekday morning, gather yesterday’s signups and post a summary to my Slack” into an automation owned by that user. An automation is a prompt: the message the agent receives at every run, with its full toolset, so each run is exactly the chat turn the user just saw work — the agent calls its tools, reads what comes back, adapts, and writes the result. The agent does the task once live, saves the prompt that reproduces it, and the chat widget shows it as a workflow card — the prompt as prose — where the user tests it (the answer streams below the card) or approves it to run on demand (“manual only”) or on a schedule preselected from their words; a change is asked for by replying in the chat, and the agent shows an updated card. Nothing is stored until the user approves: the card is the proposal, and an approval writes one user_prompts row. Each run’s result lands as a new conversation in the user’s thread list, where they can read it and ask follow-ups. What the agent carries from one run to the next is the user’s working memory, when the agent keeps one — see Memory. Everything the assistant creates is also reachable over HTTP, so your product can show a user their workflows, run one from a button, and let them pause or reschedule — with the same session token the chat widget already holds.

How the pieces fit

The building blocks a user may put in a workflow are exactly what is attached to the agent they are talking to — its tools, its toolkits (Slack, Gmail, …) and any organization workflows attached to it. Nothing else exists from the user’s point of view: an agent with only a CRM tool and Slack can only build CRM-to-Slack automations, and an agent in a network never borrows another agent’s tools.

What every agent gets

1

The builder tools are already there

Every agent carries the workflow-builder tools; there is nothing to attach and they are not listed by GET /v1/tools. They stay out of the agent’s prompt until the conversation turns to automations: the agent sees one small tool, open_workflow_builder, calls it, and from the next step on it has the tools below and the platform’s authoring guidance. Idle conversations pay for one tool schema and one sentence.
2

Attach the building blocks

Attach the tools, toolkits and organization workflows the user should be able to automate with to this agent — the agent builds with those and nothing else, and it already knows their names and input schemas. For toolkit actions the user’s own account must be connected; the agent gets list_connections and connect_toolkit for that, and schedule_user_workflow refuses with missing_connections until it is.
3

Publish the agent

Publish and activate as usual. Once the builder is open, the platform adds its own guidance block to the agent’s system prompt: the authoring contract, the list of blocks it may use, and the working agreement below. You do not need to explain any of this in your own prompt.
The agent’s part is deliberately small: find out what the user wants automated, do it once live in the conversation (the read steps only: fetch, list, search — never a send or a post), then save the prompt that reproduces what it just did. Nothing is compiled and there is no code, so there is no data shape to get wrong: at run time the agent works from what its tools actually return, retries a failed call, pages through results. The save pauses the conversation and shows the workflow card (below): the prompt, and in its footer Test and Approve — manual only, or with a schedule picked from the frequency dropdown next to the buttons. A test is optional. A test is nothing more than the automation’s instructions sent to the agent as an ordinary turn of this conversation, marked as a test: a test is a real run, every step done exactly as a scheduled run would do it, actions included, and its answer streams below the card like any answer. When it ends the card is shown again under it, so the user approves from there. To change anything the user simply replies in the chat: the agent updates the prompt and shows a new card, which replaces the old one. The agent never asks about timezones or run times: all of that happens in the card, and the user’s decision comes back to the agent as a tool result.

The workflow card

save_user_workflow does not just return: after validating the spec it suspends the agent’s turn with the card as its payload, exactly like a tool waiting for approval. Nothing is written to the database at that point: the card carries the automation itself (name, prompt, components, the schedule to preselect). The stream emits a data-tool-call-suspended part and ends; the SDK renders that part as the card; the user’s decision goes back as the tool’s resume data; the tool runs again with it — from its own input, the spec the user reviewed — and its result is an ordinary tool result in the thread. Approve is the only write: one user_prompts row is created (or the saved one updated). So the agent reacts to what the user did, and every later turn sees it in history — no side channel, no synthetic messages, no drafts, no workflow rows. Every card offers the same two actions in its footer — Test, and Approve with a frequency dropdown whose first choice is Manual only (run on demand); any other frequency turns the button into Approve & schedule and opens the time, timezone and result-thread-title fields. A line above the buttons says that a change is asked for by replying in the chat. Whatever the user does, the footer becomes the receipt of it. The card has two stages, carried as stage on the payload and on the card, which only change its badge and wording:
  1. draft — shown by save_user_workflow: the proposal, not yet tested. (draft_rev is null: nothing is stored.)
  2. tested — the card shown again after a test. A Test is not a resume and not a workflow run: the chat route answers it as an ordinary turn of the same agent on the same thread, with the automation’s prompt as the message (framed as a test), so everything a normal answer has applies — the agent’s tools, the rendering tools, streaming, memory. When the answer ends the route emits the data-tool-call-suspended part again for the same runId / toolCallId (the run is still suspended; Approve resumes it later), with stage: "tested" and test: { status: "succeeded", duration_ms, checks? } — or stage: "draft" and test: { status: "failed", error, message, checks? } when the turn errored or a check failed. checks lists every check with its verdict ({ check, passed, detail, auto? }; auto marks the ones added for the components), so the card can say what failed — “calls showTable: called 0 time(s)” — and a failed check never blocks Approve. The answer and that second part land in the message the card is in, so the SDK keeps showing ONE card: the test’s answer appears inside it, above its actions, and the card offers Test again and Approve. Every further test of the same card adds a result the card shows as a branch (a 1 / 2 picker, like a retried message) — no new card. The route reads the instructions from the pending tool call stored with the thread, so the test needs nothing in the database. (To show the card again on demand, the agent proposes it again.)
A test is optional: approving an untested proposal saves and publishes it as is. A change is not a card action — while the card is pending, the user types it in the chat; that message starts a new turn, the agent proposes again and the new card replaces the old one, which shows as superseded. The suspended part looks like this (agentVersionId and threadId are added by the API so the client can round-trip them):
prompt is the automation itself — the message the assistant receives at every run, in markdown (a numbered list when it detailed steps) — and the card renders it as prose. components are the rendering tools the answer must use, shown as tags. steps, input_schema and test_input are kept empty for clients built against the earlier step-based card. frequencies lists the schedule picker’s choices; manual means approve with no schedule. After a test the card comes back with:
test is the outcome of the test turn that just ended. The turn itself is persisted like any turn: its user message (the instructions) is tagged — with everything but the outcome from the start, the outcome when the turn ends — and hidden by GET /v1/messages, which merges the answer that followed — the card part after it — into the message the card is in, so a reload reads exactly as it did live: one assistant message holding the card and, after it, each test’s answer closed by the card part again. A test whose end was never recorded (the turn cut short) comes back closed with test: { status: "interrupted", message } once a later message shows the turn is over; the newest one stays open. The card shows the name, the prompt and the components, and offers Test and Approve. Each one is the same POST /v1/chat continuation the approval card already uses — empty messages, the chunk’s runId, toolCallId and agentVersionId, the same threadId — with the decision in approval.data. (Without agentVersionId the API recovers the agent that showed the card from the run, so a card reopened from history still resumes on the builder agent.)
The response is the continued stream: the tool result, then finish. An approval is not an agent step — the model is not called again, nothing is said, and no follow-up suggestions are made: the card itself is the receipt (the tool result carries what was saved and, for a schedule, its next run times). The result stays in the thread as the card’s tool part, so the agent’s next turn sees in its history that the automation was approved and scheduled. Use the browser’s timezone (Intl.DateTimeFormat().resolvedOptions().timeZone) and preview the run times first with GET /v1/workflow-schedules/preview if you want to show them before the user commits. A few things follow from this being a suspension:
  • While the card is pending the turn is paused. If the user types instead of using the card — the way a change is asked for — the new message starts a fresh turn; the old card stays in history as a pending call, rendered without its buttons, and the agent saves again (a new card) or shows the card again (a new proposal) when asked.
  • Each proposal is its own tool call, so a new proposal after a comment is a new card further down; older cards for the same automation (same id — a proposal:<name> id until it is saved, the prompt id afterwards) should render read-only. A Test re-suspends the SAME tool call, in the same message: render the first suspension as the card, the parts that follow it as that card’s test results (each run of parts ends with the next suspension of the call, which carries its outcome), and the newest suspension’s payload as the card’s state.
  • A card shown again after a reload is rebuilt from the thread’s metadata with the same suspendPayload, so it can be rendered and answered exactly like the live one.
The HTTP surface below (publish, draft test, schedule creation with a session token) does the same operations outside a conversation — for a “my workflows” page, not for the card.

What an automation looks like

The agent authors a small spec: a name, a description for the user, the prompt it will receive at every run, and the rendering tools its answer must use.
A run is one chat turn of the agent that built the automation: the prompt is the user message, the result conversation is the memory thread, and the agent answers with exactly what it has in the chat — its instructions, its tools, its memory, its step budget, the user’s stored credentials. Three things follow:
  • No code, no shapes. The agent never transcribes a tool’s output shape into code. At run time it reads what the tool returns, adapts, retries a failed call, pages through results — the same way it answers in chat. A small model that can do the task live can run the automation.
  • Its own tools only. The run uses exactly the tools, toolkits and organization workflows attached to the agent, with the user’s stored credentials. A tool named in the prompt that the agent does not have is simply not there. The only difference from a chat turn is that the workflow-builder tools are hidden (an automation never builds automations).
  • A test is a real run. Testing from the card sends the same instructions to the same agent in the same conversation, actions included, with the workflow-builder tools hidden as on a scheduled run, so the user sees what it really does before approving — a test, a chat reply and a scheduled run are the same turn.
Once approved, the spec is one user_prompts row (name, description, prompt, components). GET /v1/prompts/{id} returns the row.

Schedules

A schedule is structured, never a free-form cron. The agent and the user agree on a recurrence in the user’s IANA timezone: Every schedule also has:
  • timezone — an IANA name such as Europe/Paris. All times are local to it.
  • starts_at / ends_at — optional window. A schedule whose window has passed, or a once that has fired, becomes completed.
  • input — the workflow input for every run (the real recipients).
  • notify — thread (default) files each result as a conversation; none only logs the run.
  • thread_title — the title of every result thread, e.g. “Daily signups digest”.
GET /v1/workflow-schedules/preview (or the agent’s preview_schedule) answers the next run times without creating anything. The recurring minimum is hourly by construction, and the same minimum is enforced on the raw cron that backend callers may send instead of a recurrence.

Lifecycle

How runs happen

The API polls for due schedules every 30 seconds and claims each one atomically, so two API instances never double-fire. A few rules keep unattended runs sane:
  • Missed slots collapse to one. If the server was down through several fires, the schedule catches up once, from now.
  • Stale slots are skipped. A slot more than 10 minutes late is logged as skipped with skip_reason: "misfire" rather than firing a stale 08:00 digest at 14:00.
  • No overlap. If the previous run is still running, the new slot is skipped with skip_reason: "overlap".
  • One retry for transient failures. Anything but an auth, usage-limit, validation or not-found error is re-attempted once after 10 minutes, in the same result thread. The “failed” message is only posted when the final attempt fails.
  • Disabled after 5 consecutive failures. Only final outcomes count. A successful run resets the counter.
Each run is a row in the run log with its status (running, succeeded, failed, skipped), the text that was posted, any error, the thread_id of its result thread, and a request_id that joins it to its trace.

Result threads

When notify is thread, each run creates a real conversation for the user before it starts — so a long run is visible immediately — and the agent’s turn runs in it. The thread opens on a one-line user message saying when the run fired and on what rhythm (“Scheduled run of Morning digest (every weekday at 08:00 Europe/Paris) fired at …”); the automation’s instructions reach the agent as system context, not as a pasted prompt in the conversation. The answer is persisted exactly as a chat turn is: every tool the agent called (the API fetches, each with what came back), the rich-UI components (charts, tables, KPI cards, the weather widget) the automation asked for or the agent chose to add, then the text — in the same shape the chat widget already renders, so a run reads like a test, the fetch first under “Used N tools”. If the agent that built an automation is deleted or unpublished, its runs fail with a message saying so. The run log row (GET /v1/workflow-runs/{id}) carries the same text and components, plus the input the run received. These are ordinary threads with a little extra metadata:
  • metadata.source is "workflow". Filter with GET /v1/threads?source=workflow.
  • metadata.unread is true until the user opens it. Filter with &unread=true for a badge; POST /v1/threads/{id}/read clears it.
  • metadata.runStatus tracks running → retrying → succeeded / failed. While it is running, the thread’s live stream can be resumed (GET /v1/chat/stream?threadId=…) to watch the run — see Watch a run as it happens below.
A follow-up sent into the thread through POST /v1/chat routes to the agent that built the workflow, with the run — the data it fetched included — in its history, so “why is Tuesday missing?” just works. Each run is a fresh conversation: nothing thread-scoped — the history, the observer’s log of that thread — carries from one run to the next. What does carry is whatever the agent keeps per user: its working memory at resource scope (the default scope), which a run reads exactly as a chat does, since a run executes as its owner on the owner’s user record. With the observer maintaining those notes at the end of every turn (observation.manageWorkingMemory + observation.bufferOnIdle), Tuesday’s run knows what Monday’s learned, and so does the chat where the user asked a follow-up. The combination is spelled out in Memory.

Credentials

Runs execute unattended, so they cannot use the credentials on a live request. They read the user’s stored tool credentials — the same store a token mint or PATCH /v1/users/{userId} writes (see Tool credentials). When the agent schedules a workflow it captures the credentials the current request carries for the providers the workflow uses, so a schedule created mid-chat is ready to run. There is no fallback: a run whose user has no stored credentials for a provider the workflow needs fails as an auth error, and repeated auth failures disable the schedule with a reason that says so. Fix it by pushing fresh credentials from your backend (PATCH /v1/users/{userId}) and resuming the schedule, or by asking the assistant to refresh and resume.
Toolkit actions (Slack, Gmail, …) use the user’s connected account, not a credential you push. If the user disconnects it, runs fail the same way and reconnecting through the assistant fixes them.

Building your own UI on it

Your frontend can do everything the assistant does, with the session token it already holds. Every call is scoped to the session’s user: they see only their own workflows, schedules and runs, and anything else answers 404. Creating a schedule from the browser has three rules, all enforced server-side so the body cannot be used to act as someone else:
  • The schedule is created as the session’s user. user_id may be omitted, or must match — naming anyone else is a 400.
  • The billing group comes from the session, never from the body.
  • The target is one of the user’s own automations (prompt_id) or a published organization workflow (workflow_id). Another user’s automation is a 404.
The response carries the schedule with a human description (“every weekday at 08:00 Europe/Paris”), next_fire_at, next_fire_local, and the next three run times. Running from a button is a single call:
The call answers when the run is over, but the run is visible from its first second: every run that files a thread — a schedule fire, “Run now”, this call with notify_thread — streams into that thread while it runs, exactly like a chat turn. The thread appears in GET /v1/threads (and on the /v1/events feed) with metadata.runStatus: "running" as soon as the run starts; a client that opens it resumes the live stream with GET /v1/chat/stream?threadId=… — the same call that resumes an interrupted chat turn — and receives the assistant’s tool calls, components and text as they are produced, then finish when the result is written. The stream opens with a transient data-resume-state part: { "done": false } while the run is going (apply everything that follows, the turn is not in history yet), { "done": true } when a finished run is replayed within its retention window (the thread’s history already has it). The chat widget does this by itself when a conversation is opened. Once the run is over, the thread reads back through GET /v1/threads/{id}/messages as the same message the viewer watched. DELETE /v1/chat/stream?threadId=… stops a running automation from its thread; the run is logged as failed with a “stopped” message and is never retried.
A session token can read, run, schedule and delete, but never author: an automation is created and changed only in the chat, through the card the assistant shows. There is no token that lets a browser rewrite what an automation does.

From your backend

With the organization API key the same endpoints work org-wide, plus a few things a browser cannot do:
  • GET /v1/prompts?userId=… lists one user’s automations (an API key must name the user); GET /v1/workflow-schedules?owner=chatter lists every user-owned schedule in the organization, &userId= narrows to one user.
  • POST /v1/workflow-schedules takes a required user_id (the user the runs execute as: billing group, integrations, result-thread owner), an optional group, and accepts a raw cron instead of a recurrence. Use it to schedule any published workflow — yours or the user’s — for a user without a conversation.
  • GET /v1/workflow-schedules and GET /v1/workflow-runs return the whole organization, filterable by workflowId, userId, status, owner (org | chatter) and, for runs, trigger.
Schedules your backend creates are owned by the organization (owner_kind: "org"), even when they run as a user. They are managed with the API key and do not appear in that user’s session-scoped listing; schedules the assistant or the user’s own session create are owned by the user and do. Every endpoint here is also an MCP tool, so an operator’s assistant can inspect and manage schedules the same way.

Limits and defaults