Skip to content

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 a 400, as is an amount out of bounds.
  • Core sets no limit of its own. To limit every run, send cost_limit_eur on 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."}'
  • name is 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 every mail__acme-support__ tool waits except search. When several globs match one tool, the strictest mode counts (always, then model, then never), whatever their order.
  • Each name may appear once, and each entry must match a tool the request offers; otherwise the request returns 400 naming the entry. That is why the request above lists its tools.
  • Calls of an agent tool and of builtin__subagent always wait, whatever their mode, unless an entry names that tool exactly with "never". A glob or default does not let them run. When the run's agent carries its own tool_approval, the exact "never" must be in every option that names the tool, the agent's and yours.
  • builtin__tool_search never 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 your reason, 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 with approval_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_outputs returns 409: decide first, then submit any results the run asks for.
  • tool_approval is accepted only with "background": true. Agent tasks the run starts wait for approval in the same way.
  • An agent can carry tool_approval itself (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.