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.- Console
- cURL
- 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.
- Name it, choose the agent, its version, and an environment.
- Under Instructions, write what the agent should do each time.
- Add triggers: Scheduled, Slack, or GitHub. With none, the automation runs only with Run now.
- Optionally set Credential access, Files, and, under Advanced, Run metadata.
- Click Create automation. It starts paused.
201 response is the full automation, with status paused and a revision. Some fields are left out here.
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.
Required fields
Required fields
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.Retries, Idempotency-Key, and If-Match
Retries, Idempotency-Key, and If-Match
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 areschedule, 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’s0 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.
Schedule fields
Schedule fields
schedule object. Every one you save is applied to the schedule.Overlap policies
Overlap policies
Check a schedule's status
Check a schedule's status
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 themessage 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
messagetriggers 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. AddtextContainsortextMatchesso 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
@handlefrom 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.fromUserGroupIdsaccepts a person who belongs to any listed group;notFromUserGroupIdsaccepts 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 needsusergroups: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 needsusergroups: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 needchat: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 amessage, not anapp_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@nameof 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@namethat 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 needsusergroups:readandusers:read. Up to 25 targets per workspace; the API isPUT /v1/agents/{agentId}/slack-notify-targets/{connectionId}.slack_post_blocksandslack_update_messagedon’t tag targets. - One post starts one run. Slack sends a message that mentions the app as both a
messageand anapp_mentionevent. 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
continuationtoresume_threadand 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.
Slack trigger fields
Slack trigger fields
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
pendingand 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,replyFeedbackChannelIdandreplyFeedbackWakeOnNegative. One post that matches two triggers starts one run, so the API rejects triggers that disagree.
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.
Messages that never start runs, and stopping a Slack session
Messages that never start runs, and stopping a Slack session
- 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:
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
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 setfilters.ignoreThreadReplies: follow-ups in a handed-over thread are thread replies. A trigger withmemberDirectMessage,dmEnabledorprogressUsercan’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 thereply 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, asfilters.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 matchespull_request deliveries in acme/api with the action opened or synchronize on fix/* branches, and ignores drafts and bots:
git and gh in the sandbox where the deployment allows it. See GitHub access.agent-ready label:
/agent fix:
main finishes with a failure:
GitHub trigger fields and filters
GitHub trigger fields and filters
* 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. Addfilters 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:
Webhook trigger filters
Webhook trigger filters
- Paths. A
pathis dot-separated, such asdata.level, and a number indexes an array, as indata.tags.0. - Values. Write a
valueas JSON text without quotes:error,2,true, ornull. The value2matches both the number2and 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
filtersto 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
eventTypesas a security boundary; filter on a signed body field when that matters.
fieldEquals. It shows the other filters read-only and keeps them when you save.Set run defaults
runDefaults applies to every run, whether a trigger or a person starts it.
outcome and output; set them through the API. Saving the automation in the console keeps them.
Grade every run
Give an automation anoutcome, 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:
maxIterations.
Outcome fields and rules
Outcome fields and rules
- It replaces the agent version’s rubric. An automation without an
outcomegrades 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
400naming therunDefaults.outcomefield. - 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 withrunDefaults.output:
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 readpassedorcompletedreadserrorinstead, with the reasonoutput_missingoroutput_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, asoutput.nullisn’t a result. When neither holds one, the result has nooutputand the verdict doesn’t change. Ask for it in the prompt: “End your reply with the result as ajsoncode block.” - Every run keeps the path it started with. Changing
outputaffects later runs only.
Result path rules
Result path rules
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.
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 noweb_fetchtool 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_fetchand 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.
Run, pause, and resume
Start one run by hand to test the automation, then resume it so its triggers can start runs.- Console
- cURL
- In Automations, open the automation row’s Actions menu.
- Click Run now to start one run, or Run with input to add a JSON payload. Both need create permission.
- 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.
Run input, failed runs, and automatic pauses
Run input, failed runs, and automatic pauses
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.
Run filters and paging
Run filters and paging
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’sstatus 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:
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:
verdict, and show reasonMessage to people. reason is a stable code for your own branching.
passed as success, and completed too when the automation has no outcome. Treat every other verdict as not done.
Every state and reason
Every state and reason
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.verdict, and treat a reason you don’t know as information.Outcome, output, final message, and session
Outcome, output, final message, and session
state is finished.outcomeis the session’s newest outcome, when it was graded:status,terminalResult(satisfied,max_iterations_reached,not_applicable,not_evaluable, orfailed),explanation,iterations(the grading passes that reached a verdict), andcriteriafrom the last pass, in rubric order. Each criterion hascriterionId, itstext, averdictofpass,fail, ornot_applicable, an optionalrationale, and itscriterionKeywhen the rubric names it.outputis the structured result.sourceisfilefor a declared result file, with itspathandfileId, orfinal_messagewhen it came from the agent’s last message.validsays whether it is a single JSON value other thannull; when it is,jsonholds it with numbers exactly as written. When it isn’t,errorsays why, andtruncatedistruefor a file larger than the 1 MiB that is read. A result file that has expired reads asvalid: false.finalMessageis the text of the agent’s last message, cut at 64 KiB without splitting a character, withtruncatedsaying whether it was cut.eventIdnames the session event that holds all of it.sessionis a summary:status,executionState,stopReason,failure,costUsd(the billed cost as a decimal string, absent when it can’t be read),createdAt, andlastActivityAt. 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:
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 onlycurl 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.
Create a key for the job
Store the key as a secret
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.Plan for expiry
0 only for passed or completed:
- Idempotency-Key.
runAutomationrequires 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, andGITHUB_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.
--retryrepeats a request after a timeout,429, or a5xxanswer; both requests are safe to repeat. - Requiring a grade. If the job must not pass ungraded work, accept only
passed.
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.
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.- Console
- cURL
If someone else saved first
If someone else saved first
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 newagentVersionId. 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.Signing secrets and disconnecting
Signing secrets and disconnecting
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 the409 conflict response carries them in details.dependents.
Source health
Source health
Manage sources with the API
Manage sources with the API
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.
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.
Sign a timestamp to reject replays
Sign a timestamp to reject replays
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: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:{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.Every delivery response
Every delivery response
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 byInput for this run:and the input. For a scheduled run, it’s followed byThis is a scheduled run for <time>., where the time is the occurrence in UTC, such as2026-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
statusstops atcreated; 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’serror. Ifretryableistrue, run it again; otherwise fix the namedfieldor 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
errorwithoutput_missing. The agent didn’t save the declared result file. Name what the result should contain in the prompt, and readfinalMessageto see what the agent did instead.
Every automation error
Every automation error
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
artifactslist holds at most 100 files, itsfinalMessageat 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.