Run a turn in the background¶
Set "background": true to start a turn and retrieve the result later. Core returns a response id
immediately. Use background runs for long tasks, such as multiple tool rounds or large searches, or
for scheduled jobs.
Your key needs both runs and threads. A request with background returns 400 on
background if runs is disabled. Background runs must be stored, so do not set store: false.
To display output as it arrives, use streaming. Streaming and background execution cannot be combined in one request.
Work you hand a service account runs as a background run.
Start a run¶
curl -s -X POST "$CORE_URL/responses" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"model": "<a model id>", "background": true,
"input": "Summarise every open ticket for Acme.",
"callback_url": "https://your-app.example/core-callbacks"}'
Core returns 201, a Location header and "status": "queued". The model call starts after the
run is picked up. The optional callback_url receives status notifications.
Set a cost limit¶
Add cost_limit_eur to cap what one run may spend, in euro: a number or a decimal string above
zero, at most 1,000,000, with at most six decimals.
curl -s -X POST "$CORE_URL/responses" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"model": "<a model id>", "background": true, "cost_limit_eur": "2.50",
"input": "Summarise every open ticket for Acme."}'
The limit covers the run itself, the agent tasks it starts and their runs, the agents it calls as
tools, and the turns Core starts to answer its tasks. Before each model call Core adds up what all
of them have spent, at your organisation's prices. Once that reaches the limit, the run stops: it
ends incomplete with incomplete_details.reason: "cost_limit", and what it produced so far is
kept and billed. A model call already under way is finished first, so the final spend can end a
little above the limit. A task whose run the limit stopped fails with cost_limit.
- The limit is accepted only with
"background": true; on any other request it is a400, as is an amount out of bounds. - Core sets no limit of its own. To limit every run, send
cost_limit_euron every request. - The response shows the limit back as
cost_limit_eur. - Your key's and your organisation's monthly caps still apply beside it.
Poll for results¶
curl -s "$CORE_URL/responses/resp_0KQ2mR..." -H "Authorization: Bearer $CORE_KEY"
Poll every few seconds; the endpoint does not support long polling. A successful run moves from
queued to in_progress to completed. The usage field is updated during execution.
status |
Next step |
|---|---|
queued, in_progress |
Poll again. |
completed |
Read output. |
failed |
Read error.code. |
incomplete |
The run reached its tool-round limit or its cost limit, or the model reached its output limit (A cut-off answer). incomplete_details.reason says which. |
requires_action |
Submit the pending tool results, or decide each mcp_approval_request in output (Approve a tool call). |
cancelled |
No further action is needed. |
Receive callbacks¶
Core sends a status notification to callback_url when the run finishes or pauses for tool results:
{ "type": "response.status", "response_id": "resp_...", "status": "completed", "request_id": "req_..." }
Fetch the result with GET /responses/{id} using your API key. The callback contains a reference to
the response, not its output.
Deliveries include User-Agent: artific-core and a stable Idempotency-Key. Use that key to
deduplicate retries of the same notification. Once your key has a callback secret, each delivery
also carries Artific-Signature; see Verify a callback.
To hear about every run of your organisation on one connection, or on one webhook, instead of one callback per run, use events.
The callback host must be an http or https host on your organisation's
allowed hosts; rules about hosts in your key's
policy apply to it too. Core never sends a callback to an address on a private network. A key with
a region restriction only reaches a callback host its policy places in one of those regions. Core checks the host when creating the run and
again before sending the callback. A refused host at creation returns 403 without creating a
run. A non-2xx callback response triggers retries with increasing delays. Core stops retrying if
the host is no longer allowed.
Verify a callback¶
A callback is signed only for a key that has a callback secret. Create one for the key you start runs with:
curl -s -X POST "$CORE_URL/key/callback-secret" \
-H "Authorization: Bearer $CORE_KEY"
{ "object": "api_key.callback_secret", "key_id": "key_...", "secret": "whsec_..." }
Store the secret now; Core does not show it again. GET /key only says callback_secret_set. A
key holding management can do the same for another key of its organisation with
POST /keys/{key_id}/callback-secret, and for a key of an organisation below it with
POST /organisations/{organisation_id}/keys/{key_id}/callback-secret
(Hand out, rotate and withdraw keys).
When the request is refused, nothing was stored and the key keeps the secret it had:
| Status | What it means | What to do |
|---|---|---|
503 |
this deployment holds no key to seal the secret with | ask the operator of the deployment |
409 |
the key is being changed by another request | try again |
403 |
on /key/callback-secret: the request was made with a subject token. On a route that names a key: the parent organisation reserved key management |
call with the API key itself, or ask the parent organisation |
404 |
on a route that names a key: the key is not one you can reach, or your key does not hold management |
check the key id and the calling key's capabilities |
From then on every callback of that key carries
Artific-Signature: t=<unix seconds>,v1=<signature>, the same header and the same signature as an
event webhook. Verify it with the function under
Verify the signature, over the raw body bytes, and refuse a callback that
arrives without the header.
- A key without a callback secret keeps sending callbacks unsigned. Treat an unsigned callback as a hint to fetch the run, and read the result with your own key rather than trusting the body.
- Calling the route again replaces the secret. Callbacks sent from then on, retries included, verify only against the new one.
- The callback secret is separate from the key's own secret: rotating the key leaves it unchanged, and deleting the key deletes it.
- If Core cannot read the stored secret, it sends no callback for that run instead of sending one
unsigned. Poll
GET /responses/{id}if a callback you expect does not arrive.
Submit tool results¶
A run pauses at requires_action when your application needs to execute a tool. Submit the results
to resume it:
curl -s -X POST "$CORE_URL/responses/resp_0KQ2mR.../tool_outputs" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"outputs": [{"call_id": "call_9x...", "output": "{\"open\": 12}"}]}'
Provide exactly one result for each pending call. Missing or extra results return 400. A run that
is not waiting for results returns 409. An accepted submission moves the run back to queued.
Core checks the current policy when execution resumes. Tools that your key can no longer access will not run.
List a service account's runs¶
A service account runs in the background by itself:
its schedules fire, other keys hand it work, its own key starts turns. A key holding management and threads
lists every run the account made, newest first, without keeping its own index:
curl -s "$CORE_URL/service-accounts/sva_0KQ2mR.../responses?status=requires_action" \
-H "Authorization: Bearer $CORE_KEY"
Each item names the run's id, status, created_at, completed_at, thread_id, the
schedule_id that fired it, and the key and person that handed it over
(instructed_by_key_id, instructed_by_subject_id); read its output with GET /responses/{id}.
Narrow the list with:
| Parameter | Keeps |
|---|---|
status |
runs in that status; repeat it for several, for example ?status=requires_action&status=failed |
schedule_id |
runs that schedule fired |
from, to |
runs created from from (inclusive) up to to (exclusive); a to before from returns 422 |
The list pages with limit and cursor like every other list. It shows only the runs your key's
rules let it see, as GET /responses/{id} does. An account of another organisation
returns 404. The account's own key, which never holds management, and a subject token cannot
use this route.
Approve a tool call¶
Add tool_approval to have a person approve tool calls before they run. Give each tool a mode:
always waits for a person every time, never runs at once, and model lets the model decide,
following your instructions. Tools you do not list take default, which is never.
curl -s -X POST "$CORE_URL/responses" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"model": "<a model id>", "background": true,
"tools": [{"type": "tool", "name": "mail__acme-support__search"},
{"type": "tool", "name": "mail__acme-support__send"},
{"type": "tool", "name": "api__acme-crm"}],
"tool_approval": {"timeout_seconds": 2592000,
"tools": [{"name": "mail__acme-support__*", "approval": "always"},
{"name": "mail__acme-support__search", "approval": "never"},
{"name": "api__acme-crm", "approval": "model"}]},
"instructions": "Ask for approval before you change a customer record.",
"input": "Send Acme the overdue invoice reminder."}'
nameis a tool's name as the request offers it, or a glob over names in the same form a toolset uses (*,?,[...]). An API connector is one tool,api__<connector>; a mail connector has one tool per operation,mail__<connector>__<operation>. An exact name wins over every glob, so above everymail__acme-support__tool waits exceptsearch. When several globs match one tool, the strictest mode counts (always, thenmodel, thennever), whatever their order.- Each name may appear once, and each entry must match a tool the request offers; otherwise the
request returns
400naming the entry. That is why the request above lists its tools. - Calls of an agent tool and of
builtin__subagentalways wait, whatever their mode, unless an entry names that tool exactly with"never". A glob ordefaultdoes not let them run. When the run's agent carries its owntool_approval, the exact"never"must be in every option that names the tool, the agent's and yours. builtin__tool_searchnever waits, whatever its mode: it only searches the tools your key may use and runs none of them.
For a tool in model mode, Core adds one more required parameter, request_approval. The model
sets it to true for a call that needs approval. Core takes the parameter out before the call
runs, so your tools never receive it. A call marked true, or one where the model left the
parameter out, waits; only false runs at once. A custom (freeform) tool in model mode gets no
request_approval parameter and always waits.
Changed on 2026-10-23: what {} means
Before per-tool modes, "tool_approval": {} let the model decide for every tool. Now nothing
waits except calls of agent tools and builtin__subagent. Send {"default": "model"} to get
the old behaviour. Runs queued and schedules created before the change keep the behaviour
they had.
When a call waits, the run moves to requires_action, your callback_url is told, and the event
feed carries run.approval.requested. The run's output shows each waiting call as an
mcp_approval_request item:
{"id": "apr_0KQ3...", "type": "mcp_approval_request", "server_label": "api",
"name": "api__mailer__send", "arguments": "{\"to\":\"billing@acme.example\"}"}
Decide with the item's id:
curl -s -X POST "$CORE_URL/responses/resp_0KQ2mR.../approvals/apr_0KQ3..." \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"approve": false, "reason": "Wait until the payment run on Friday."}'
{"approve": true}runs the call.{"approve": false}never runs it; the model reads yourreason, at most 2,000 characters, as the call's result.- The first decision counts. A second decision on the same call, or a decision on a run that no
longer waits, returns
409. - Once every waiting call is decided, the run moves back to
queued. Approved calls run before the model's next step, under your key's policy at that moment. timeout_seconds(60 to 31,536,000) is how long one wait may last. When it passes, the run fails withapproval_expired. Leave it out to wait with no deadline: a call then waits for as long as the run exists.- While a call waits,
tool_outputsreturns409: decide first, then submit any results the run asks for. tool_approvalis accepted only with"background": true. Agent tasks the run starts wait for approval in the same way.- An agent can carry
tool_approvalitself (Agents through the API). Your request can then only make it stricter.
Who is asked, how they are reminded, and the screen they answer on are up to your application.
Cancel a run¶
curl -s -X POST "$CORE_URL/responses/resp_0KQ2mR.../cancel" -H "Authorization: Bearer $CORE_KEY"
Cancellation takes effect between tool rounds. An active call finishes before the run stops.
Cancelling a run that has already finished returns 409. Cancelling a run that was already
cancelled returns the run, without an error. A streamed turn is cancelled the same way; see
Stop a streamed turn.
Run failures¶
A run can fail if its permissions or resources change during execution:
error.code |
Meaning |
|---|---|
key_revoked |
The key was deleted. |
tenant_unavailable |
The organisation, or an organisation above it, was suspended, or the organisation is being deleted. The run is not picked up again when the suspension is lifted; send it again. |
usage_cap_exceeded |
Your key's or your organisation's monthly cap was reached before a round began. The rounds the run had finished are kept. A round the cap stops partway ends the run incomplete instead, with incomplete_details.reason: "usage_cap_exceeded". |
capability_withdrawn |
runs or threads was disabled for the key. |
tool_unavailable |
A tool offered to the run no longer exists. |
egress_denied |
A required destination is no longer allowed. |
agent_required |
While this run waited, the key's policy came to require an agent, or a rule came to forbid a turn without an agent for the person it is for, and the request named none. Send it again naming one of the key's agents. |
model_denied |
The key may no longer use the model, or the model no longer runs in the region the request asked for. Check GET /models for what the key may call now, and drop hosting.region or ask for a region that matches. |
approval_expired |
A call waited for approval longer than timeout_seconds and nobody decided it. The run took no further step after that call. |
reaped_too_often |
The worker running the response stopped three times in a row without finishing a round, so Core gave it up rather than try again for ever. Send it again; if it fails the same way, the request itself is what stops the worker, so tell your administrator. |
Limitations¶
- Per-request MCP authorization headers. Core does not store these headers. Use an MCP server that does not require a credential supplied with each request.
- Images. Background execution currently cannot load image content, including uploaded images referenced by
file_id. Use a synchronous image request or provide a text description instead.