Skip to main content
An automation saves everything a session needs: a pinned agent version, an environment, an opening prompt, and run defaults such as credentials and files. Its triggers decide when it starts a session, and every start is recorded as a run, so you can see why each session began and what it was given. Each run also has a result: one request that waits for the run to finish and answers with its verdict, its structured JSON result, and the files it saved, so a CI job can start a run and act on how it ended. You can manage automations in the console, under Automations, or through the REST API with an API key. Both work on the same automations.
You need an agent version and an environment. Saving checks that both exist. Runs need the environment’s setup verified: a run on an unverified environment fails with environment_not_verified. See Agents and Verify the setup script. Slack and GitHub triggers also need an event source: connecting Slack or GitHub on Integrations creates one. See Receive events. Creating automations and starting runs by hand needs create permission, changing, pausing, and resuming them needs update, and deleting needs delete. Organization developers and admins have all of them. See Organizations and roles.

Create an automation

Choose the agent version, environment, and instructions, then add the triggers that should start runs.
  1. In the sidebar, click Automations, then click New automation. To start from a ready-made automation, click Start from a template at the top of the form and choose one; it fills in the name, triggers, and instructions, and you still choose the agent and environment.
  2. Name it, choose the agent, its version, and an environment.
  3. Under Instructions, write what the agent should do each time.
  4. Add triggers: Scheduled, Slack, or GitHub. With none, the automation runs only with Run now.
  5. Optionally set Credential access, Files, and, under Advanced, Run metadata.
  6. Click Create automation. It starts paused.
A 201 response is the full automation, with status paused and a revision. Some fields are left out here.
The ETag response header is an opaque validator; send it unchanged as If-Match on the next change. Do not construct it from the body’s revision. New automations start paused: test one with Run now from its row’s Actions menu, then resume it so its triggers can start runs. See Run, pause, and resume.
Every request field is required: displayName, agentId, agentVersionId, environmentId, initialPrompt, runDefaults (at least metadata, which can be null), and triggers (an empty list for manual runs only). You choose each triggerId yourself, as a UUID, and keep it stable across replacements. A trigger id stays with its automation: a new trigger needs a new UUID, even after the trigger or automation that had the old one is deleted.
Copy the complete returned ETag, quotes included. A missing If-Match returns 428 precondition_required; a stale one returns 412 precondition_failed. A public contract change can invalidate an ETag even when the body’s revision is unchanged: read the resource again before retrying. See Idempotent mutations and Errors.

Choose triggers

An automation has up to 20 triggers, and a run starts when any enabled one matches. The trigger types are schedule, slack, github, and webhook. Turn a trigger off with enabled: false to keep it without it starting runs. You can also start a run by hand at any time, even while the automation is paused.

Schedule

Starts runs on a cron expression read in a time zone, such as the create example’s 0 9 * * 1-5 in Europe/London: 9:00 on weekdays. By default, an occurrence is skipped while the previous run’s session is still going.
These fields go inside the trigger’s schedule object. Every one you save is applied to the schedule.
The console shows each schedule’s next and last run, upcoming runs, and occurrences skipped for overlap or missed outside the catch-up window. It also shows whether each saved schedule is in effect. getAutomationStatus returns the same values for each schedule trigger:

Slack

Starts a run for Slack messages. It needs a Slack event source: connect Slack on the Integrations page. Choose the message or app_mention event and the channels to accept. This trigger starts a run for each new top-level post in an incident channel that mentions a severity, and ignores other Slack apps:
  • Filter message triggers by text. Without a text filter, every new message in the trigger’s channels starts its own run and a paid session, with no cap. The editor warns about this but still saves. Add textContains or textMatches so only the messages you mean start work.
  • Filter by sender group. For channel messages, add Sender is in user group or Sender is not in user group and choose an @handle from the selected Slack connection, or enter a Slack user-group ID (S…) directly. The picker saves the group’s ID, so renaming its handle does not change the filter. fromUserGroupIds accepts a person who belongs to any listed group; notFromUserGroupIds accepts a person who belongs to none. When both are set, both conditions must hold. These filters cannot be combined with DM access. Membership comes from the connected workspace and is refreshed at most every 30 seconds. A deleted or disabled group makes only the trigger that references it fail closed; temporary membership read failures retry the delivery. Bot and app messages do not match either condition. The Slack installation needs usergroups:read; reconnect an older installation if it lacks that scope.
  • Call an agent by a Slack user-group @handle. Open the agent’s Slack tab to create a group in a connected workspace or associate an existing group by its S… ID. The group’s mention handle is also the name used for agent replies when that handle is active; otherwise replies use the agent name. Then choose Agent mentioned in the automation’s Slack trigger; the selected agent’s group is used automatically. Creating or changing a group through RMA needs usergroups:read, usergroups:write, and workspace permission to manage user groups. To give replies a custom picture, set Slack reply picture on the agent’s General tab to an HTTPS image URL Slack can fetch without signing in, or an emoji such as :robot_face:. Custom reply names and pictures need chat:write.customize; reconnect older installations to request the scopes. Adding people to the group would notify them on every mention. Slack sends a user-group mention as a message, not an app_mention. RMA matches the ID in Slack’s mention token, so renaming the group’s handle does not require changing the trigger. Clear handle removes the binding so the agent can be set up with another group. A matched group-directed post takes precedence over broad message triggers on the same event source. When no Agent mentioned trigger matches, a post that tags an agent’s active handle goes to that agent’s own Slack message triggers, never to another agent’s, and an AI filter on them always wakes the agent for it. A post that tags the handles of two agents starts nothing. Mentions of workspace groups that no agent owns, such as an on-call rotation, are ordinary text: they leave broad triggers eligible, and an AI filter judges the message by its content.
  • Let an agent tag Slack groups and people. On the agent’s Slack tab, Can tag lists the user groups and people the agent may tag in each connected workspace. Give each a name and a one-line description of when to tag it; the agent sees both. In a post the agent writes @name, and the first standalone @name of each target becomes a live mention. A user group is tagged only in the Slack thread the session started from; a person can be tagged in any post the agent makes. If the agent writes a listed group in Slack’s own <!subteam^…> form, which it may copy from thread messages, it’s treated as that group’s @name. Any other <!subteam^…> group, @here, @channel, and any @name that isn’t on the list stay plain text. When a listed group stays plain text because of where the agent posted, or a <!subteam^…> group isn’t on the list, the tool result tells the agent why. Saving checks each group and person against the workspace, which needs usergroups:read and users:read. Up to 25 targets per workspace; the API is PUT /v1/agents/{agentId}/slack-notify-targets/{connectionId}. slack_post_blocks and slack_update_message don’t tag targets.
  • One post starts one run. Slack sends a message that mentions the app as both a message and an app_mention event. One post starts one run per automation, even when a trigger lists both events or two of its triggers each match.
  • Every Slack event starts a new session, unless the trigger continues its thread. By default a reply in the same thread starts another run with its own session. Set continuation to resume_thread and a later matching post in a thread is delivered to that thread’s session instead, with its transcript and workspace. See Continue a Slack thread.
The console lists up to 1,000 channels the connected Slack app is a member of. Invite the app to a channel to make it selectable. The same list is available from the API for a Slack connection:
The people and apps for fromUserIds come from the connection’s member catalog, read with the same connection:

Make an agent available in Slack DMs

In the automation’s Slack trigger, enable Available in Slack DMs. In the API, set "dmEnabled": true on that trigger. Choose Direct messages only (or set "events": [] with "dmEnabled": true) to accept DMs without channel events. DM access defaults to off. Keep the automation active and its trigger enabled for it to appear in the picker. DM the Recursion app in Slack, choose an automation, and reply with your task in the conversation thread it opens. Each thread talks to the selected agent; start a new message to Recursion to choose another agent or start a fresh task. If an agent has several available automations, their automation names distinguish the choices. The automation supplies its saved agent version, environment, credentials, and budget. The From filter restricts who can see and use the automation in DMs. Without it, users in the connected workspace can invoke it. Channel, mention, event, and text filters continue to control channel traffic; they do not restrict DMs. Existing Slack connections may need reconnecting to grant im:history and im:read. The Slack app must also subscribe to message.im events and have its Messages tab enabled. Turn off DM availability to remove the automation from the picker and stop new messages reaching it through existing DM threads. Pausing the automation or changing its agent also stops those conversations. Group DMs and externally shared DMs are unsupported.

Continue a Slack thread

With "continuation": "resume_thread", an automation keeps one conversation per Slack thread. Every matching post still records a run, and the run’s sessionAction says what it did:
  • A conversation belongs to one automation and one thread. Two automations that both continue the same thread keep separate sessions, and a thread in another channel, workspace, or organization never reaches this one.
  • A resumed session keeps its agent version. Publishing a version and moving the automation’s pin changes new threads only. To change the version a thread’s session runs, move that session.
  • Posts that arrive together join one session. The first one starts it and the rest are appended, each exactly once, in the order they were admitted.
  • A refusal that can pass is retried, not replaced. If the session is still stopping, its input queue is full, or the tenant is out of credit, the run stays pending and is delivered when it can be.
  • A thread’s runs have no result of their own. They share one session, so reading a run’s result is refused with 422 automation_run_session_shared. Read the thread’s session instead.
  • Give every Slack trigger of an automation the same continuation, firstResponse, replyFeedbackChannelId and replyFeedbackWakeOnNegative. One post that matches two triggers starts one run, so the API rejects triggers that disagree.
A common shape is two triggers on one automation: message with ignoreThreadReplies for new top-level posts in a channel, and app_mention for replies that mention the app, including replies in that same channel.

Collect feedback on replies

With "replyFeedbackChannelId" set, each reply the agent posts with slack_post_message in the thread that woke it ends with Slack’s thumbs up and thumbs down buttons. A click records the rating at once and opens a form, Do you have any additional feedback?, with one optional text box. Sending a comment adds it to that rating’s card; closing the form changes nothing. In the console, open the automation and use Message feedback, which appears once the automation has a Slack trigger. Turn on Add feedback buttons to replies and choose the Feedback channel. The console lists that Slack account’s channels when every Slack trigger uses the same account; otherwise type the channel ID (C…). To have a thumbs down wake the agent, also turn on On thumbs down, wake the agent with the feedback so it can retry, which needs every Slack trigger to continue the thread’s session. The console writes both settings to every Slack trigger, as the API requires. An automation managed from an agent folder says so in that section: change the setting in the folder, because the next recursion managed-agents apply replaces an edit made in the console. Each click posts one card to the channel, with the rating, the rater, the source channel, the agent, and a link to the reply; a comment sent from the form is added to that card. The card copies nothing else from the reply. Anyone who can see the reply can rate it, and a rater from another Slack workspace is marked external when Slack names their workspace. Repeat clicks post repeat cards.
  • Only a thumbs down can reach the agent, and only with "replyFeedbackWakeOnNegative": true. It wakes the thread’s session with the rating so the agent can look at its reply again and retry, and a comment sent from that click’s form follows as a second event. Each reply wakes the agent at most once: a later thumbs down on the same reply, and its comment, only post to the channel. The rater must be someone whose message in that channel the trigger would accept, under its shared-channel and sender rules. Otherwise no run starts, and a rating costs no model usage.
  • Invite the app to the channel first. A card the app cannot post is retried for about two hours and then dropped.
  • Sessions started before you set this keep no buttons, including their later replies. Removing it stops new sessions from getting buttons; buttons already posted keep working.
  • Some posts get no buttons: direct messages, posts to other channels, scheduled posts, Block Kit posts and file uploads.

Acknowledge a Slack post

With "firstResponse": "platform", the platform reacts to the post, posts an acknowledgement in the thread, and edits that acknowledgement into a one-line status while the agent works. When the turn ends the status line is removed, or replaced by a line saying the run stopped if the session failed or was cancelled. It posts as the Slack app that received the event, with the bot token of the workspace’s Slack connection on the Integrations page. The reaction needs the reactions:write scope, which a Slack connection made before that scope was requested does not hold: reconnect Slack to add it. Without it the acknowledgement and status line still appear. When the workspace has no active Slack connection for that app, the trigger behaves as none: the run is unaffected, and a failed run still posts its failure note.
  • Only newly posted messages start runs. Editing or deleting a message never starts one.
  • Stopping a Slack-started session posts nothing to its thread. The console reminds you when you force-stop or interrupt one. Reply in the thread yourself, or message the agent to post an update.

Continue a conversation from any trigger

"continuation": "resume_thread" works on Slack, GitHub, webhook, and schedule triggers. IMAP triggers do not take a continuation; every email starts its own session. Each event names its conversation, and every later event of the same conversation reaches that conversation’s session, with the same sessionAction values as a Slack thread: Manual runs never continue a conversation. Give every trigger of one type in an automation the same continuation; the API rejects triggers that disagree. Each event the session receives also carries a channel-neutral envelope in the run’s eventContext.envelope: its channel, actor, text, conversation key, and where a reply would go. When a trigger routes unowned conversations to a coordinator (below) and coordinator mode is enabled for the organization, the message each event delivers ends with a Conversation key: <key> line naming its conversation, for example Conversation key: slack:A0123456789:T0123456789:C0123456789:1700000000.000100. The coordinator uses the key to hand that conversation to a sub-agent. After coordinator mode is turned off for the organization, a conversation the coordinator already holds still reaches it, and its messages keep the line. Every other trigger’s messages carry no such line.

Route new conversations to one coordinator

Experimental: behaviour and shape may change. Coordinator mode is available only to organizations where it is enabled; elsewhere a trigger with "unowned": "coordinator" is refused.
By default a conversation no session holds yet starts its own session. Set "unowned": "coordinator" on a Slack, GitHub, or webhook trigger to deliver every such conversation to one long-lived coordinator session instead. Each event source has its own coordinator: a Slack app, a GitHub App installation, and each webhook source never share one, so a delivery from one source never reaches a session another source’s events steer. The first event from a source starts its coordinator; later new conversations are appended to it, and a conversation the coordinator hands to a sub-agent keeps reaching that sub-agent.
  • A coordinator trigger continues conversations. It requires "continuation": "resume_thread", and a Slack coordinator trigger cannot set filters.ignoreThreadReplies: follow-ups in a handed-over thread are thread replies. A trigger with memberDirectMessage, dmEnabled or progressUser can’t use it: each keeps one person’s conversation private or acts as one person, and a coordinator answers many.
  • Give every trigger of one type on one event source the same unowned. The API rejects triggers that disagree; triggers on different sources may differ. Schedule and IMAP triggers don’t take it.
  • The coordinator is replaced when it can’t continue. If it ended, refuses messages, ended at its budget, or the automation now names another agent, the next new conversation starts its successor, and conversations the old coordinator held reach the successor. A conversation whose sub-agent is gone also returns to the coordinator.
  • Every new conversation from a source reaches one session. Route a dedicated channel to a coordinator trigger, or filter the trigger, so the coordinator doesn’t read unrelated chatter.

Reply in the conversation

A session that an automation starts from a Slack or GitHub event gets the reply tool, except one started from a member’s own Slack DM. It answers in the conversation the session started from: the Slack thread or DM, or the pull request or issue as a comment. A sub-agent that its requester hands such a conversation to also gets reply, and answers in that conversation. When a trigger continues conversations, only the session that the conversation’s next message would reach can reply in it. A session that hands a conversation to a sub-agent, or that a successor replaces, stops replying there. reply works without a Slack or GitHub integration grant on the agent. Slack and GitHub tools the agent was granted keep working as before. It can post only where the event came from your connected Slack app or the GitHub App: for an event from a source you verify with your own signing secret, reply says there is no conversation to reply in. Sessions started any other way don’t get reply. A Slack reply is posted by the same Slack tools as slack_post_message, so it carries the agent’s reply name and picture and follows the same reply review, approval, and status line. A GitHub reply is a comment by the GitHub App, posted only while the repository still has the same GitHub id; a renamed or recreated repository doesn’t receive it. Either way reply uses only tools the connection’s tool selection allows agents. To ask a question, the agent passes 2 to 8 choices and ends its turn. The answer is the conversation’s next event, so reply refuses choices unless the trigger continues conversations ("continuation": "resume_thread", or a Slack DM) and admits an answer: block_actions or message in a Slack trigger’s events (not with mentionUserGroupId), or issue_comment in a GitHub trigger’s. Every answer must also pass the trigger’s filters, so a trigger with any filter gets no choices. On Slack the AI classifier is the exception, because it never judges replies or clicks, and a DM trigger is not filtered. In Slack the choices are buttons when the trigger admits block_actions (a button label is at most 75 characters), and a click reaches the session as a block_actions event naming the choice’s value. Otherwise, and on GitHub, the choices are a numbered list and the person answers in the thread or with a comment. GitHub admission ignores the App’s own reply comments, so a reply never wakes its own session.

Filter events with an AI filter

GitHub, webhook, and IMAP triggers take the same AI filter as Slack, as filters.classifier. After the trigger’s other filters pass, a decision model answers your question about the event’s text (the comment or review, the delivery body, or the email’s subject and text), and a run whose answer says to skip settles skipped without starting a session. Each evaluation is billed as a classifier call.

GitHub

Starts a run for GitHub webhook events. It needs a GitHub event source: connect GitHub on the Integrations page. This trigger matches pull_request deliveries in acme/api with the action opened or synchronize on fix/* branches, and ignores drafts and bots:
The event only starts the run. To read or change the repository, the agent needs a GitHub grant: the GitHub MCP tools, plus authenticated git and gh in the sandbox where the deployment allows it. See GitHub access.
Three more common triggers. This one hands a pull request to the agent when someone adds the agent-ready label:
This one starts a run when a pull request comment contains the command /agent fix:
This one starts a run when a CI workflow run on main finishes with a failure:
In globs, * matches any characters, including /, and ? matches one character.

Webhook

Starts a run for deliveries to a custom webhook source. The console offers this trigger when your organization has a custom source. Add filters to start runs only for some deliveries. A delivery must meet every condition in every list. This trigger starts runs only for critical or error deliveries of the incident.opened event that have an owner and aren’t muted:
  • Paths. A path is dot-separated, such as data.level, and a number indexes an array, as in data.tags.0.
  • Values. Write a value as JSON text without quotes: error, 2, true, or null. The value 2 matches both the number 2 and the string "2". Objects and arrays never equal a value.
  • Limits. The four body lists hold up to 20 conditions together. Each list you send needs at least one entry; leave out a list you don’t use, and leave out filters to match every delivery. Filters read only the body and the event type, never other headers.
  • Event types aren’t signed. The signature covers the body, not the header that names the event type. Don’t treat eventTypes as a security boundary; filter on a signed body field when that matters.
The console edits fieldEquals. It shows the other filters read-only and keeps them when you save.
To send deliveries to the source, see Send a custom webhook delivery.

Set run defaults

runDefaults applies to every run, whether a trigger or a person starts it. The console has no fields for outcome and output; set them through the API. Saving the automation in the console keeps them.

Grade every run

Give an automation an outcome, and every session it starts is graded against it, whether a trigger or a person started the run. The run’s result then says whether the work passed. Use it when “the session finished” isn’t proof enough, for example before a CI job merges what an agent produced. An outcome is an objective and a rubric. Each time the agent finishes its work, a grader checks it against every criterion in the rubric and either accepts it or sends the agent back with what fell short, until the rubric is met or the grading passes run out. Write outcome rubrics explains how to write criteria a grader can check. Add outcome to runDefaults when you create or replace the automation:
The automation read returns the outcome with its effective maxIterations.
  • It replaces the agent version’s rubric. An automation without an outcome grades its runs against the stored rubric of the agent version it runs, with the run’s opening message as the objective, and runs them ungraded when that version has none. See Grade every session of an agent.
  • Saving checks the rubric. A rubric that breaks the rules is refused with 400 naming the runDefaults.outcome field.
  • Each run keeps the outcome it started with. Changing the automation’s outcome affects later runs only.
  • Grading adds to each session’s cost. Every pass runs in its own linked session, and its usage counts toward the run’s session. See Pricing.

Ask for a structured result

When a program reads each run, ask for a JSON result it can parse instead of prose. Declare a result file with runDefaults.output:
Agents are told to save their final result at that path as a single JSON value. Say in the prompt what the JSON should contain, for example “The result has issues, the number of issues triaged, and high, the list of high-severity issue numbers.” The agent saves the file as a deliverable, and the run’s result reads it back as output.
  • A declared file is required. If the agent never saves the file, or saves something that isn’t a single JSON value or is null, a run that would have read passed or completed reads error instead, with the reason output_missing or output_invalid. Every other verdict stays as it is.
  • Without a declared file, the final message is read. When the agent’s last message is a single JSON value, or holds code blocks labelled json, the result reports that value, or the last block’s, as output. null isn’t a result. When neither holds one, the result has no output and the verdict doesn’t change. Ask for it in the prompt: “End your reply with the result as a json code block.”
  • Every run keeps the path it started with. Changing output affects later runs only.
path is relative to the deliverables folder, as a write_artifact path is: result.json or reports/result.json. It is refused with 400 on runDefaults.output.path when it:
  • is empty, or longer than 1,024 bytes;
  • starts with /, or with the deliverables folder itself;
  • has . or .. segments, a doubled slash, a trailing slash, or spaces around it;
  • ends in a file name that is empty, longer than 255 characters, or has control characters.
These rules apply when the path is saved. A replacement that sends the stored path unchanged keeps it.The result reads at most 1 MiB of the file. A larger file reads as valid: false with truncated: true, and the verdict turns to error with output_invalid. The verdict doesn’t check the JSON against a schema; check its shape in your own code.

Keep untrusted input in check

Event runs put outside text into the session: Slack messages, pull request and issue titles and bodies, comments, and webhook bodies. Treat that text as untrusted input. Someone who can post in the channel or open an issue can try to steer the agent.
  • Restricted environments drop web_fetch. A session an automation starts, or a session started from a Slack thread, gets no web_fetch tool when its environment limits internet access (limited or no access). The environment’s network policy is then its only way out.
  • Unrestricted environments keep web_fetch and full internet access. An injected instruction could ask the agent to send data anywhere. Use a limited environment for automations that read outside text, and grant only the credentials and repositories the task needs.
  • Webhook runs record only the sender’s headers. The event in a webhook run keeps the headers the sender sent. Headers added on the way to the event source are never recorded.
See Environments and Security.

Run, pause, and resume

Start one run by hand to test the automation, then resume it so its triggers can start runs.
  1. In Automations, open the automation row’s Actions menu.
  2. Click Run now to start one run, or Run with input to add a JSON payload. Both need create permission.
  3. Click Resume to let its triggers start runs, or Pause to stop them.
runAutomation answers with the run. What success means: the run moves from pending to created, and sessionId names the session it started. Follow that session like any other; see Sessions. To wait until the work is done and learn how it ended, get the run’s result. Resume returns the automation with status active. Pausing stops triggers from starting runs; Run now still works. Each change raises the revision, so use the new ETag for the next one.
payload is optional and can be any JSON value up to 64 KiB (65,536 bytes) as JSON; a larger one is refused with 400 on payload.A run that couldn’t start a session ends failed with an error that has a code, a message, the field or resourceId responsible when there is one, and retryable.Resume is refused with 422 automation_reference_invalid while the automation’s environment is deleted, or while it names an agent or agent version that no longer exists; the automation stays paused. If a run fails because something the automation uses no longer exists, such as its agent, environment, a vault, a credential, or a file, the automation is paused, and the run’s error says what is gone. Fix the automation, then resume it.

See run history

In Automations, choose View runs from the automation row’s Actions menu. Each run shows its Trigger, when it started, and Session creation. Filter by status, trigger, or date, and click a run for its Run detail and a link to its session. In the API, list an automation’s runs, then read one. getAutomationRun returns one run with its frozen snapshots and effectiveInitialMessage.
listAutomationRuns returns runs newest first. Filter with automationId, triggerId, eventSourceId, triggerType, status, createdAtGte, and createdAtLt, and page with limit and cursor.

Get a run’s result

A run’s status says only whether its session started. Its result says how the work ended: whether it is still going, and once it is finished, a verdict with a reason, the graded outcome, the structured JSON result, the agent’s last message, and the files the session saved. One request can wait for it, so you don’t need to follow the session yourself. A run of a Slack trigger that continues a thread, or answers a DM, shares its session with the thread’s other runs, so its result would show whichever run spoke last. Reading it is refused with 422 automation_run_session_shared, even while the run is pending; read the session instead. Every other run starts its own session and has a result. Reading a result needs read access, which every organization role with Managed Agents access has. Pass waitSeconds, up to 20, to hold the request until the run finishes or the time is up:
While the run is going, the answer is short: state is pending or running, with run and, once there is one, session. Repeat the request until state is finished, since a busy organization’s request can answer before the time is up. A finished run answers at once:
Act on verdict, and show reasonMessage to people. reason is a stable code for your own branching. To gate a CI job, treat passed as success, and completed too when the automation has no outcome. Treat every other verdict as not done.
A finished run reads running again when its session continues. Someone can continue it, for example by replying to a session that stopped for review, raising its budget, or sending a completed session another message. Some sessions also continue on their own: a browser hand-off (awaiting_human) when its deadline passes, and a session paused for credit once credits are bought.New reasons can appear. Branch on verdict, and treat a reason you don’t know as information.
These parts are present only when state is finished.
  • outcome is the session’s newest outcome, when it was graded: status, terminalResult (satisfied, max_iterations_reached, not_applicable, not_evaluable, or failed), explanation, iterations (the grading passes that reached a verdict), and criteria from the last pass, in rubric order. Each criterion has criterionId, its text, a verdict of pass, fail, or not_applicable, an optional rationale, and its criterionKey when the rubric names it.
  • output is the structured result. source is file for a declared result file, with its path and fileId, or final_message when it came from the agent’s last message. valid says whether it is a single JSON value other than null; when it is, json holds it with numbers exactly as written. When it isn’t, error says why, and truncated is true for a file larger than the 1 MiB that is read. A result file that has expired reads as valid: false.
  • finalMessage is the text of the agent’s last message, cut at 64 KiB without splitting a character, with truncated saying whether it was cut. eventId names the session event that holds all of it.
  • session is a summary: status, executionState, stopReason, failure, costUsd (the billed cost as a decimal string, absent when it can’t be read), createdAt, and lastActivityAt. Read the session for everything else.

Download the files a run saved

artifacts lists every file the run’s session saved as a deliverable, including files its subagents and teammates saved, sorted by path. It is empty until the run is finished. Download a file by its fileId:
Each entry has fileId, path under the deliverables folder, filename, mediaType, byteSize, createdAt, downloadable, and sha256 when it is known, so you can check a download. Download only entries with downloadable set to true. The list holds at most 100 files; when the session saved more, artifactsTruncated is true, and listing the session’s files returns all of them.

Run an automation from CI

A CI job can start a run, wait for its result, and pass or fail on the verdict, with only curl and jq. Create the automation first, with an outcome when “it finished” isn’t enough and a result file when the job reads a value.
1

Create a key for the job

Create an API key that only this job uses, with the narrowest scope that works: one organization. The key acts as the person who creates it, so they need a role there that can start runs, such as Developer, and the key stops working if they leave the organization. To keep the job independent of any one person, an admin or owner can instead create a service account in Settings › Service accounts, with a role such as Developer, and create the job’s key on its page.
2

Store the key as a secret

Save the key in your CI system’s secret store, such as a GitHub repository or environment secret named RECURSION_API_KEY, and expose it to the job as the RECURSION_API_KEY environment variable. Never print it, commit it, or put it in the automation’s prompt.
3

Plan for expiry

Every key expires, at most 365 days after it is created. Note the date, and rotate the key before it expires: the old key keeps working for the overlap you choose while you update the secret.
This script starts a run with the commit as input, waits up to 30 minutes, cancels the session if time runs out, and exits 0 only for passed or completed:
  • Idempotency-Key. runAutomation requires one. A key made from the CI run and attempt numbers makes a retried request return the same run, while a re-run of the job starts a new one. The script uses GitHub Actions’ GITHUB_RUN_ID, GITHUB_RUN_ATTEMPT, and GITHUB_SHA; in another CI system, use its equivalents.
  • Waiting. Each result request waits up to 20 seconds and answers as soon as the run finishes, so the loop makes about three requests a minute. A busy organization’s request can answer at once, so the loop pauses 2 seconds before asking again. --retry repeats a request after a timeout, 429, or a 5xx answer; both requests are safe to repeat.
  • Requiring a grade. If the job must not pass ungraded work, accept only passed.
To fetch what the run saved, download its artifacts after the loop:

Run it in GitHub Actions

This workflow runs the automation on every pull request opened from a branch of the repository, writes the verdict to the job summary, and fails the check unless the run passed or completed. Save it as .github/workflows/agent-check.yml, and add the key as a repository secret named RECURSION_API_KEY. GitHub passes the secret to the step as an environment variable and masks it in the logs. GitHub gives no repository secrets to pull requests from forks or to runs started by Dependabot, so the job skips those instead of failing on an empty key. Don’t switch to pull_request_target to get around this: it runs with your secrets on code nobody has reviewed yet.
To follow a run that a trigger started rather than one the job started, find it with listAutomationRuns, as in See run history, and wait on its result the same way.

Change or delete an automation

Saving a change keeps the automation’s current status. Deleting it removes its triggers while preserving records for runs that already started.
In Automations, use the automation row’s Actions menu to open it for editing or to delete it. The automations list is newest first, and the editor shows the automation’s prompt and triggers. Click Save changes to save an edit.
Deleting an agent deletes the automations that run it. See Delete an agent.
If someone else saved the automation while you were editing it, Save changes saves nothing and shows This automation changed elsewhere. Nothing was saved. Choose Keep my edits to replace their version with yours on the next save, or Load the newer version to see what they saved.

Keep the agent version current

An automation keeps the agent version it was saved with. Later changes to the agent’s instructions, tools, and integrations don’t apply to its runs until you move it to the new version. In the Automations list, an Older version label marks automations that run an older version than their agent’s latest. When you open one, the drawer names both versions, for example “This automation runs Version 1 of backend-fixer. The agent is now on Version 2”, and offers Use Version 2. Click it, then Save changes. In the API, replace the automation with the new agentVersionId. Moving an automation doesn’t move sessions it already started; move those sessions themselves.

Receive events

An event source is an endpoint that receives events and checks they’re genuine. Automations listen to it through Slack, GitHub, and webhook triggers. One source can serve many automations, up to the enabled-trigger limit in Limits. Sources created by connecting an account are connection-managed: they start active, and you pause and resume them from the list.
Slack and GitHub sources check every delivery against a signing secret that the connection creates and manages. Signing secrets never reach an agent sandbox. Disconnecting the account removes its source and stops its triggers. Existing automations keep those trigger settings so you can select a new source after reconnecting.

Manage sources

In Automations, open the Event sources tab. Each source’s health and last delivery are columns in the list, and its row’s Actions menu pauses or resumes deliveries. A healthy source has received a delivery, which does not prove the provider can reach it right now. A connection-managed source is changed and removed through its connection on Integrations. A custom source can be deleted when no automation has a trigger on it. A switched-off trigger still holds its source, although the list’s Triggers column counts only the ones that are on. A refused delete names the automations: the console lists them, and the 409 conflict response carries them in details.dependents.
listEventSources and getEventSource return sources, and getEventSourceStatus returns health, triggerCount, and lastDelivery. Pause and resume need If-Match with the source’s ETag. Set EVENT_SOURCE_ETAG to the complete header returned by getEventSource.

Send a custom webhook delivery

A custom source receives deliveries at its Callback URL, shown in Event sources. The full URL is the console’s address followed by the callback path, https://recursion.labelbox.com/agents/v1/event-sources/<eventSourceId>/events. Give it to the sender, and resume the source when you’re ready to receive events. Send a JSON body with POST to the callback URL. For an hmac_sha256 source, compute the HMAC-SHA256 of the exact body bytes, keyed with the source’s signing secret, and send it as lowercase hexadecimal in the signature header, after the prefix. Sign the bytes you send; re-serializing the JSON after signing breaks the signature.
A delivery that’s accepted returns 200 with {"ok": true}, and matching triggers on active automations start runs. The same signed body is treated as the same delivery, so a retry doesn’t start a second run. For an unsigned source, send an X-Webhook-Id or Idempotency-Key header to name the delivery. To route deliveries by kind, send an X-Managed-Agents-Event-Type header of up to 256 bytes and filter on it with eventTypes.
An hmac_sha256_v2 source signs ordered parts instead of only the body, so it can match a sender that signs a timestamp with the body. List up to 8 parts in signedParts, in the order the sender joins them, with no separator added between them:Add replayWindow to reject old deliveries. Its header carries the send time in whole Unix seconds, such as 1790000000, and must also be a signed header part. A delivery whose timestamp is missing, isn’t digits only, or is more than maximumAgeSeconds (1 to 3600) before or after its arrival fails with 401 unauthorized, so a captured delivery can’t be replayed later. This verification policy matches Slack’s v0:{timestamp}:{body} signature:
For a {timestamp}.{body} signature, sign the timestamp header, the literal ., then the body. The signature is still lowercase or uppercase hexadecimal after the prefix; base64 signatures and headers that carry several signatures aren’t supported. Headers that the platform adds or removes on the way in, credentials such as Authorization, and the signature header itself can’t be signed. An hmac_sha256 source keeps signing only the body. The console shows an hmac_sha256_v2 source read-only, so create and change one with createEventSource and replaceEventSource.

How automations run

  • Every run uses the pinned agent version. Publishing a new agent version doesn’t change the automation. See Keep the agent version current.
  • Everything is frozen per run. A run records the automation, trigger, and event exactly as they were, and the session’s opening message. Editing the automation later doesn’t change runs already started.
  • The opening message is your saved prompt. For an event run, it’s followed by Event for this run: and the event as JSON. For a manual run with input, it’s followed by Input for this run: and the input. For a scheduled run, it’s followed by This is a scheduled run for <time>., where the time is the occurrence in UTC, such as 2026-10-01T08:00:00Z. When the automation declares a result file, the message ends with the sentence that asks for it.
  • The verdict comes from the session. A run’s status stops at created; its result follows the session to a verdict.
  • Sessions show where they came from. A session an automation started shows Started by in its session panel: the automation, the trigger (Manual, Schedule, Slack, GitHub, or Webhook), and the run, each linked.

What can go wrong

The most common problems:
  • A run is failed. The session couldn’t start. Read the run’s error. If retryable is true, run it again; otherwise fix the named field or resource.
  • An automation paused on its own. A run found that something it uses was deleted. Read the failed run’s error, fix the automation, then resume it.
  • An event starts no runs. Check the source status and the trigger filters. Edited or deleted Slack messages never start runs.
  • Runs still use access you removed from the agent. The automation runs its pinned agent version. Move it to the new version. See Keep the agent version current.
  • A run’s result reads error with output_missing. The agent didn’t save the declared result file. Name what the result should contain in the prompt, and read finalMessage to see what the agent did instead.
For every error code, see Errors.

Limits

Limits on triggers, prompts, names, run input, run defaults, run results, schedules, trigger filters, and custom webhook bodies are in Limits. Three more apply here:
  • A page of automations or event sources holds 1 to 100 items, 50 by default.
  • A result request waits at most 20 seconds. Its artifacts list holds at most 100 files, its finalMessage at most 64 KiB, and at most 1 MiB of a declared result file is read.
  • Name and prompt limits count UTF-8 bytes, not characters: an emoji or a non-Latin letter takes 2 to 4 bytes, so a name in those scripts fits fewer than 256 characters.

Next steps

Integrations

Connect Slack and GitHub to get event sources.

GitHub access

Give automated runs access to the repositories they work on.

Outcome rubrics

Write the rubric every run is graded against.

Files

Mount the same files into every run.

Sessions

Follow the sessions automations start.