Skip to main content
An agent with the Workflow Builder tools can turn a conversation like “every weekday morning, gather yesterday’s signups and post a summary to my Slack” into a real workflow owned by that user, test it with them, and put it on a schedule. Each run’s result lands as a new conversation in the user’s thread list, where they can read it and ask follow-ups. 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 builder agent: its tools, its toolkits (Slack, Gmail, …) and any organization workflows attached to it. Nothing else exists from the user’s point of view, so an agent with only a CRM tool and Slack can only build CRM-to-Slack automations.

Setting up a builder agent

1

Attach the Workflow Builder tools

The builder tools are static tools under the workflow-builder provider. List them with GET /v1/tools and add them to the agent’s agentTools like any other tool. All thirteen belong together — attach the whole group.
2

Attach the building blocks

Attach the tools, toolkits and organization workflows the user should be able to automate with. 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. 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 follows a fixed working agreement: discover what the user needs, save a draft, fix any issues the save reports, run a test and check the result together, confirm the timezone and preview the next run times, then schedule. Runs from a test really post to Slack and really send email, which is why the spec carries a test_input with safe targets (the user’s own address, a test channel) that the test uses by default.

What a user workflow looks like

The agent authors a small linear spec, deliberately simpler than the full workflow definition your Studio uses: an ordered list of up to 15 steps, each with an id, reading values from the workflow input ($.input.<field>) or from an earlier step ($.steps.<id>.<path>).
Two conventions matter for anyone reading these specs:
  • Recipients and destinations are inputs. Anything that addresses the outside world — channels, email recipients, webhook URLs, folder ids — must be a workflow input, never a literal in a step. The test run fills those from test_input; the schedule supplies the real values as its input.
  • output_text is the message the user reads. It is a template rendered against the run and posted to the result thread. Without it the user gets a plain completion notice with the raw output attached.
The spec is compiled into an ordinary workflow definition and stored next to it, so GET /v1/workflows/{id} returns both: the compiled definition the runtime executes and the authoring spec the agent re-reads and patches. The compiler allow-lists every tool and workflow name against what the builder agent has attached.

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).
  • notifythread (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”.
  • copilot_id — which widget the result thread is listed under.
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 posts the result when it finishes. The thread carries the workflow invocation, the rendered output_text and any rich-UI components (charts, tables, KPI cards) the workflow produced, in the same shape the chat widget already renders. 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 runningretryingsucceeded / failed.
A follow-up sent into the thread through POST /v1/chat routes to the agent that built the workflow, with the run in its history, so “why is Tuesday missing?” just works.

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 workflow must be one of the user’s own or a published organization workflow. Another user’s workflow 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:
A session token can read and schedule, but never author. Saving a draft, publishing and deleting a workflow stay with the assistant’s tools or the organization API key — there is no token that lets a browser rewrite what a workflow 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/workflows?owner=chatter lists every user-built workflow in the organization; &userId= narrows to one user; ?owner=all mixes them with your Studio-authored ones.
  • 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