Features

Runs

How an agent executes: lifecycle, ways to launch, inputs, results, logs and limits.

A run is one execution of an agent in its own sandbox. A run has an id (run_ followed by a UUID), a per-agent runNumber, a status, a result and an audit trail that records who or what launched it.

Lifecycle

pending → running → success | failed | timeout | cancelled
StatusMeaning
pendingThe run exists, the sandbox is being provisioned.
runningThe agent loop is executing.
successThe run finished and its output (if a schema is declared) is valid.
failedAn error, an invalid output, a model failure, or a stall detected by the watchdog.
timeoutThe run exceeded its timeout.
cancelledSomeone cancelled it.

Terminal runs carry completed_at, duration (milliseconds), cost (US dollars) and token_usage (see Run cost).

Launching a run

From the web app

Open an agent and click Run. If the agent declares an input schema, a form generated from it appears. The run page streams logs and status as the run progresses (see The run page).

From the API

curl -X POST https://your-instance/api/agents/@acme/support-triage/run \
  -H "Authorization: Bearer apst_your_key" \
  -H "Idempotency-Key: 2f1c0b0e-launch-001" \
  -H "Content-Type: application/json" \
  -d '{ "input": { "max_tickets": 10 } }'

The call is fire-and-forget: it answers 201 with the created run resource (the same shape as GET /api/runs/{id}, with status usually pending or already running) and execution continues in the background. The route is rate-limited to 20 requests per minute and honours Idempotency-Key.

The body is closed (an unknown field is a 400) and every field is optional:

FieldMeaning
inputValues for the agent's input schema. File fields take upload://, appfile:// or inline data URIs (see Files).
rerun_fromA previous run id whose stored input is replayed. Mutually exclusive with input. The run must belong to the same agent (409 rerun_agent_mismatch).
modelIdModel for this run only (system model key or organization model id).
generationPer-run temperature and reasoning_level.
proxyIdProxy for this run, or "none" for direct egress.
connection_overridesPer-integration connection picks, as arrays of connection ids.
dependency_overridesPer-run version overrides for skills and integrations. "draft" requires write access to that package.

Query parameter version chooses the agent definition: published (default, the latest published version), a version, dist-tag or semver range, or draft for callers who can write the agent. The run records the definition it used in version_ref.

Headers worth knowing: X-Space-Id (browser sessions), Appstrate-User (impersonation), Appstrate-Version (API versioning), and X-Appstrate-Connect-Offers: 1, which asks a 409 missing_integration_connection to carry a ready-to-open connect link for each missing integration.

Launch failures that create no run: 400 model_not_configured or 400 model_credential_missing when no usable model is configured, 404 agent_not_active_in_space, 404 no_published_version, 422 dependency_unresolved, 422 bundle_invalid, 409 missing_integration_connection, 402 when a billing module refuses usage, 413 when input files exceed their caps.

Waiting for the result

Poll GET /api/runs/{id}, or let the server hold the request:

curl "https://your-instance/api/runs/run_0194...?wait=55" \
  -H "Authorization: Bearer apst_your_key"

wait accepts true or a number of seconds, capped at 55. The call returns when the run reaches a terminal status or the wait elapses, so a non-terminal status means "ask again". Each identity can hold 10 concurrent waits. To watch live progress, use the realtime streams. The run_and_wait tool of the MCP server launches and waits in one call.

Rerunning

Send rerun_from instead of input. Files attached through upload:// are kept as durable appfile:// references, so a rerun does not need them uploaded again. Inline data: files are not replayable (409 rerun_inline_input_unavailable): stage them with an upload when the input must be replayable.

Inline runs

An inline run executes an agent defined entirely in the request, without publishing a package first.

curl -X POST https://your-instance/api/runs/inline \
  -H "Authorization: Bearer apst_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "manifest": {
      "name": "@inline/summarize-contract",
      "version": "0.0.0",
      "type": "agent",
      "schema_version": "0.3",
      "display_name": "Summarize contract"
    },
    "prompt": "Summarize the attached document.",
    "context_files": ["appfile://file_..."]
  }'

The body takes manifest and prompt (required), input, context_files (existing appfile:// files mounted read-only), connection_overrides, modelId, proxyId and generation. The platform stores a temporary shadow package (package_ephemeral: true on the run), runs it through the normal pipeline, and keeps the manifest and prompt snapshot for the retention window (default 30 days) before compacting them.

Referenced skills and integrations must already exist in the organization or system catalog. Composing a manifest is authoring and launching it is running, so the caller needs both agents:write and agents:run.

POST /api/runs/inline/validate runs the same preflight (manifest, input, integration readiness) without creating anything. It has the same per-minute cap as inline runs, counted separately.

Remote runs

A remote run (runOrigin: "remote") executes the agent on the caller's host, for example the appstrate run CLI or a CI job, and streams signed events back to the platform. The platform still tracks status, logs, files, cost and liveness. The CLI side, and when to prefer a remote run over a platform run, is in CLI.

POST /api/runs/remote creates the run. The caller needs agents:run (a body that ships its own manifest also needs agents:write), and the route honours Idempotency-Key. The body is closed:

FieldMeaning
sourceRequired. Either { "kind": "registry", "packageId": "@scope/name" } with optional stage (draft or published, default published), spec and integrity, or { "kind": "inline", "manifest": {...}, "prompt": "..." }.
spaceIdRequired. It must match the space of the request (the key's space, or X-Space-Id).
inputValues for the agent's input schema. A file field cannot point at a platform-stored file (upload:// or appfile://): the platform has no workspace on the caller's host. Use an inline data: URI.
dependency_overridesSame meaning as on a platform launch.
contextSnapshotFree-form object describing the runner, at most 16 KiB once serialised. The CLI sends its OS, its version and the bundle's name and version.
sink.ttl_secondsHow long the signing credentials stay valid, up to 86400. The default and the ceiling are operator settings (REMOTE_RUN_SINK_DEFAULT_TTL_SECONDS, REMOTE_RUN_SINK_MAX_TTL_SECONDS).

There is no modelId or proxyId: a remote run resolves no platform model, and a body that carries them is refused with 400. There is no connection_overrides either. A remote runner addresses one connection per integration, because its api_call tool carries no connection argument. A run whose connections cannot be resolved is refused with 409 missing_integration_connection and one errors[] entry per integration, as on the other launch routes. When the connection choice binds several connections to one integration, that entry's code is remote_binds_one_connection (field: integrations.<id>): narrow it to one with a member pin (an admin narrows a set that an admin pin or an enforced organization default imposes), or run the agent on the platform. A must_choose_connection entry is also cleared with a member pin. The response holds the run id and the one-time sink credentials (url, finalize_url, secret, expiresAt). The runner posts its events to url, signed with secret, and closes the run through finalize_url. PATCH /api/runs/{id}/sink/extend with ttl_seconds extends the lifetime of a sink for a run that lasts longer than planned, and events posted after it expires are refused. In the web app a remote run carries a Remote badge, and its Cancel button is hidden, because the platform cannot signal the caller's host. See Run liveness for how a silent runner is finished.

LLM proxy and credential proxy

A remote run has no sandbox on the platform, so it reaches the organization's models and the space's integration credentials through two authenticated HTTP surfaces of the instance. A script, a CI job or another service can call them directly. Their request and response schemas are in the generated API reference: LLM proxy and Credential proxy.

SurfaceRouteCredential required
LLM proxyPOST /api/llm-proxy/<api shape>/..., four routes (below)An API key carrying llm-proxy:call, or the access token of a user whose organization role holds it. The owner, admin, member and guest roles do.
Credential proxyAny method on /api/credential-proxy/proxyAn API key carrying credential-proxy:call, or the access token of a user who holds it in the space. The admin and builder space roles do, operator, runner and viewer do not.

Both accept bearer authentication only. A cookie session is refused with 403. The "access token of a user" is the device-flow token of the CLI, or a dashboard access token. A key follows its creator and its scopes, as described in API keys, and neither scope should be given out lightly (a key created with scopes omitted or empty carries both when its creator holds them): the first spends the organization's model budget, the second reaches every provider the space has connected.

LLM proxy. One route per protocol, each shaped like the vendor's own endpoint, so a vendor SDK works when its base URL points at the proxy:

API shapeRouteSDK base URL
openai-completions/api/llm-proxy/openai-completions/v1/chat/completionshttps://your-instance/api/llm-proxy/openai-completions/v1
openai-responses/api/llm-proxy/openai-responses/v1/responseshttps://your-instance/api/llm-proxy/openai-responses/v1
anthropic-messages/api/llm-proxy/anthropic-messages/v1/messageshttps://your-instance/api/llm-proxy/anthropic-messages
mistral-conversations/api/llm-proxy/mistral-conversations/v1/chat/completionshttps://your-instance/api/llm-proxy/mistral-conversations

The request body is the vendor's, with one change: model is the id of a model preset of the organization (the id that GET /api/models and appstrate models list print), not a vendor model name. The platform resolves the preset, puts the real model id and the provider key in, forwards the call and relays the answer, streamed or not. Each call is metered in the usage ledger. A preset whose protocol does not match the route you called is a 400, and so is a preset that comes from an OAuth subscription, which the proxy never serves. Send the key as Authorization: Bearer: the proxy does not read x-api-key, so a vendor SDK that sends that header by default (the Anthropic SDK does) needs the bearer header added. With a user access token instead of an API key, also send X-Org-Id. The optional X-Run-Id header attributes the call to a run so its cost rolls up into that run; for a user token it must be a run that user launched. appstrate models list --proxy-only lists the presets these four routes can serve.

When the operator turns on the response cache (LLM_PROXY_CACHE_MODE=simple, off by default), a non-streaming 2xx answer can be replayed from a cache private to the organization and the preset. The Cache-Status header then reads appstrate-llm-proxy; hit for a replayed answer, or appstrate-llm-proxy; fwd=uri-miss; stored when the upstream was called and its answer stored.

curl https://your-instance/api/llm-proxy/anthropic-messages/v1/messages \
  -H "Authorization: Bearer apst_your_key" \
  -H "Content-Type: application/json" \
  -d '{ "model": "<preset id>", "max_tokens": 256, "messages": [{ "role": "user", "content": "Hello" }] }'

Credential proxy. It sends one HTTP request to a third-party API with the credentials of a connection injected on the platform, so the caller never holds them. The method is the one you use, and the proxy forwards it. Control headers say what to call:

HeaderMeaning
X-Space-IdThe space to act in. Optional with an API key, which already names its space (a different value is refused with 403). Required with a user access token.
X-Org-IdThe organization id. Required with a user access token, ignored with an API key.
X-Integration-IdThe integration package, for example @scope/name.
X-TargetThe upstream URL. It must match the authorized_uris of the integration's auth.
X-Session-IdA UUID v4 that you choose. It scopes the server-side cookie jar and is bound to the first principal that used it.
X-Run-IdOptional. The in-flight run of the calling actor this call acts for. It confines the call to the connections that run was bound to.
X-Connection-IdOptional. A connection uuid, when several could serve the call.
curl -X GET https://your-instance/api/credential-proxy/proxy \
  -H "Authorization: Bearer apst_your_key" \
  -H "X-Space-Id: spc_..." \
  -H "X-Integration-Id: @scope/name" \
  -H "X-Target: https://api.example.com/v1/items" \
  -H "X-Session-Id: 3b241101-e2bb-4255-8caf-4136c566a962"

The answer is the upstream's status, headers and body, with a Proxy-Status header that tells a relayed upstream answer from one the proxy produced itself. Cookies the upstream sets are kept in the session's jar and sent back on later calls with the same X-Session-Id until their Max-Age or Expires passes (the sidecar of a platform run keeps the same kind of jar for the run). While it lives, a cookie set by the target's own origin takes precedence over the injected credential of the same name; once it expires it is dropped and the credential is sent again. A call for an integration that is not active in the space, or with an X-Run-Id that names no run of the space, is a 404 not_found. A 404 credential_not_found means no connection of the integration is reachable for the caller. When the proxy refuses or fails the call itself, the problem's code is one of the api_call failure codes the sidecar reports to an agent (unauthorized_target, blocked_target, credential_exfiltration_refused, upstream_unresolvable, credential_unusable, upstream_unreachable, upstream_timeout), listed with their statuses in Errors. The credential-proxy route limits request and response size and the call rate, and the LLM proxy limits the request size and the call rate. The values are operator settings (LLM_PROXY_LIMITS, CREDENTIAL_PROXY_LIMITS) in Environment Variables.

How appstrate run uses them. A remote run started by the CLI sends every {ns}__api_call tool call of an integration to the credential proxy, with the integration as X-Integration-Id, one random X-Session-Id for the whole run and, when the run is reported, its id as X-Run-Id. With --model-source preset it lists the presets, picks one (--model, or the instance default) and points the model's base URL at the matching LLM-proxy route, sending the preset id as model. The CLI itself never sees a provider key or an integration credential. With --model-source env it calls your provider directly and the LLM proxy is not involved. The flags are in CLI.

Results

A finished run exposes:

  • result.output: the structured value passed to the output tool, validated against output.schema when one exists. It is null while the run is in flight or when no output was emitted.
  • error: a message when the run did not succeed.
  • Files: deliverables the agent wrote under ./outputs/ or published with publish_file (see Files). file_counts gives the number of input and output files. artifacts summarises the end-of-run sweep (complete or partial, with the names of any files that could not be stored).
  • checkpoint: the carry-over snapshot of the run (see Memory).
  • connections_used: the connections bound to the run, for display.

Logs

curl "https://your-instance/api/runs/run_0194.../logs?level=info&limit=500" \
  -H "Authorization: Bearer apst_your_key"

The response is a list envelope. since=<id> returns entries with a greater id and is the cursor for tailing. level is a minimum severity (debug, info, warn, error). limit defaults to 1000 and, when more follow, a Link: rel="next" header points at the next page. The route is limited to 120 requests per minute. Tool results inside log data are truncated at write time (2048 bytes by default).

The run page

Open a run from Runs, from an agent's runs tab or from a notification. The page is the same for a running and a finished run, and it updates live while the run is active.

Banners. Above the tabs, up to three notices can appear:

  • The run's error message, when it ended in anything but success and carries one (grey for a cancelled run, red otherwise).
  • Connection lost during this run, when an integration returned an authentication error that survived a refresh. The run did not fail, the agent just lost that tool. Each integration listed has a Reconnect link (shown when your role can open the integration page). The platform records these ids in the run's metadata.degraded_integrations.
  • Some files were not saved, when the end-of-run sweep could not store some of the files the agent produced. Each file is listed with a reason: file too large, storage quota exceeded, run no longer active, upload failed after retries, or not saved. Those files are lost, and re-running the agent regenerates them.

Tabs. There are always four, in the same order, so a link such as #execution always lands somewhere.

TabWhat it holds
OutcomeWhat the run produced: the featured file, the list of produced files, the output value and the memory it wrote. A run that produced none of them shows "This run produced nothing", and the Execution tab says why.
FilesEvery file attached to the run, with a filter: All, Imported (consumed by the run) or Produced. Only the first 100 are listed, and the tab says so when there are more. Its badge counts both directions.
ExecutionHow it ran: the logs, run details, usage and per-turn table, the input payload and the run's metadata.
ConfigurationHow it was set up: the agent, the version (v1.2.0, or draft, with + modifications when a draft differs from the published base), who or what triggered it and the connections it used, each tagged with how it was chosen. An inline run shows its prompt and manifest here, until the retention window compacts them.

The page opens on Outcome when the run produced a file, an output value or a memory write, and on Execution otherwise. That choice is made once when the page settles, so a file published while you read does not move you to another tab. The URL hash (#outcome, #files, #execution, #configuration) selects a tab, and an unknown hash falls back to the default.

Featured file. When the run produced exactly one file, the Outcome tab opens it in a viewer above the list, with its name and a download button. With none or several, nothing is featured and the files are only listed. A file the run merely read as input never counts, even one an earlier run produced.

Readouts. At the right of the tab strip:

  • Cost. The run's cost in dollars with four decimals. While the run is active a dot pulses and the figure follows the run live. Two caveats show as a tooltip: a bare dash in place of the amount means the model has no price, so the cost could not be computed (a zero would read as "free"), and a figure followed by an asterisk is a floor, because part of the consumption (an unpriced call or cached input) had no rate.
  • Context gauge. A bar and 128k / 200k counts for how full the model's context window is. While the run is active it shows the current context of the last turn, with a percentage. Once the run ends it shows the peak instead, because a run that compacted its context ends far below the point it reached. The gauge is absent when the runner reported no turns or no context window for the model, rather than showing an empty bar.
  • Re-run (not on a running or an inline run, and it needs agents:run) and Cancel (on an active run, needs runs:cancel, hidden for a remote run).

The total token count is not in the header. Open the Run details panel (the chevron at the end of the run row above the tabs) and read Total tokens, whose tooltip splits it into input, output, cache read and cache creation tokens. It adds up every turn of the run and is not the size of the current context.

Execution tab. Its sections, top to bottom:

  1. Logs, with a count badge, described below.
  2. Run details: run id, runner (Platform or Remote, followed by the runner's name), duration, start and completion times, and the proxy when one was used.
  3. Usage, with a Live badge while the run is active: cost, model, and input, output, cache creation and cache read tokens. A run with no usage data says so.
  4. Per-turn breakdown, present only when the run reported turns. Per turn it lists the context sent to the model (input plus cache reads and writes, output excluded), the share of the model's window when it is known, the output tokens and the latency. When the window is unknown the bars are relative to the highest turn instead.
  5. The input payload and the run's metadata, when they are not empty.

Tool-call trace. The log viewer shows the agent's own messages, log tool lines and runtime breadcrumbs, with an icon for the level (debug, info, warn, error). A tool call is a single row: the call and its result are joined on the tool call id, so parallel calls of the same tool that settle out of order stay on their own rows. The row shows a status, the tool name, a short form of the arguments and the duration when the runner measured one:

StatusMeaning
Tool runningThe call started and has no result yet, and the run is still active.
Tool completedThe result arrived and was not an error.
Tool failedThe result was flagged as an error.
Tool interruptedThe call started, the run ended, and no result ever arrived.
Tool status unknownAn older call row without a tool call id, in a run that has ended, so it cannot be paired.

Click a tool row to open Tool details with the status, the duration, the full Arguments and the Result. A result is truncated when it is written to the log (see Logs). The toolbar above the viewer toggles Timestamps, shows or hides tool rows, turns Auto-scroll back on (scrolling up turns it off) and copies the logs with Copy logs: the copy covers the rows currently visible, with their arguments and results.

Degraded runs

Some things the platform cannot give a run are not errors, so the run starts anyway and records the gap as a warn entry in the run's logs. The entries have type system, and their data carries platform: true. They are written once, right after the run is created and before the sandbox's own logs, for runs the platform launches, and a failure to write one never fails the run. The run's status is not changed by them. Without them, a run that started with fewer tools would look the same as an agent that chose not to use them. In the web app they appear as warning lines in Execution, Logs. Over the API, read GET /api/runs/{id}/logs (level=warn narrows it) and look for these event values.

eventWhat it means
integration_droppedThe agent declares an integration that could not be started, so its tools are unavailable to this run. One entry per drop.
generation_setting_droppedA stored generation setting (temperature or reasoning_level, from the space's defaults or a schedule's override) is refused by the run's model, so the run goes on without it. One entry per setting.
model_fallbackThe model chosen for the run is no longer usable, so the run uses the default model instead.

integration_dropped carries integrationId, a reason, and optionally a detail and a connectionLabel. The connection label is set when the drop concerns one bound connection. Several connections bound to one integration form a set that starts whole or not at all: when one of them has no usable credential, the integration is dropped, the lost connection with no_delivery and each of the others with bound_set_incomplete.

reasonCause
not_foundNo package with that id.
not_integrationThe package is of another type (the detail names it).
invalid_manifestThe integration's manifest fails validation.
not_activeThe integration is not active in the space.
remote_source_invalidA remote integration has no usable url and transport.
local_server_ref_missingA local integration names no server package.
mcp_server_unresolved, mcp_server_not_runnableThe MCP server package it references could not be resolved, or has no runnable configuration.
no_deliveryThe bound connection is no longer reachable, or it has no usable credential (for example one that cannot be decrypted).
bound_set_incompleteAnother connection of the same bound set had no usable credential.
resolve_errorAn unexpected error while resolving it, with its message in detail.

generation_setting_dropped carries setting, value, model and reason: "refused_by_model". model_fallback carries pinnedModelId, model and reason: "pinned_model_unavailable". Read these entries when an agent seems to be missing a tool or ignoring a setting. An integration whose tool selection is empty is skipped without an entry, because there is nothing to expose. An integration whose source.server.version pin cannot be satisfied is a different case: the launch fails with 422 dependency_unresolved and no run starts.

Cancelling

curl -X POST https://your-instance/api/runs/run_0194.../cancel \
  -H "Authorization: Bearer apst_your_key"

The call returns the updated run, already cancelled, once the sandbox has been stopped. It requires runs:cancel and applies only to runs you may read. A run that is already terminal answers 409 not_cancellable.

Listing and deleting

GET /api/runs lists the runs of the current space. Filters: user=me, kind (all, package, inline), status, start_date, end_date, chat_session_id, with limit (max 100) and offset. GET /api/agents/{scope}/{name}/runs lists one agent's runs.

Visibility follows roles and permissions: runs:read shows the runs you launched, runs:read-all shows every run of the space. DELETE /api/agents/{scope}/{name}/runs deletes the completed runs of an agent and needs runs:delete plus runs:read-all. It is refused while a run of that agent is in progress.

Who launched it

Every run records the identity it ran as, and how it was triggered. A run has an actor, either userId or endUserId, and may also carry the key or schedule that triggered it:

FieldMeaning
userIdThe member the run executed as (dashboard, CLI, MCP, chat). For a run started with an API key, the member who created the key.
endUserIdThe end-user the run executed as, instead of a member.
apiKeyIdSet in addition to the actor when a server-side API key launched it.
scheduleIdSet in addition to the actor (the schedule's actor) when a schedule fired.

The response adds readable names (user_name, end_user_name, api_key_name, schedule_name), plus runner_name and runner_kind for remote runs. model_label, model_source and proxy_label freeze which model and proxy the run used.

Limits

These are operator settings. The values below are the defaults. The variables are described in Environment Variables.

LimitDefaultSetting
Run timeout ceiling (a manifest timeout is clamped to it)1800 sPLATFORM_RUN_LIMITS.timeout_ceiling_seconds
Timeout when the manifest declares none300 sfixed
Concurrent runs per organization50PLATFORM_RUN_LIMITS.max_concurrent_per_org
Runs started per organization per minute200PLATFORM_RUN_LIMITS.per_org_global_rate_per_min
Agent memory and CPU ceiling1536 MiB, 2 vCPUPLATFORM_RUN_LIMITS.agent_memory_ceiling_mb, agent_cpu_ceiling
Inline runs per minute60INLINE_RUN_LIMITS.rate_per_min
Inline manifest / prompt size65536 / 200000 bytesINLINE_RUN_LIMITS.manifest_bytes, prompt_bytes
Skills in an inline run20INLINE_RUN_LIMITS.max_skills
Inline shadow retention30 daysINLINE_RUN_LIMITS.retention_days
Files per run (input and output)200RUN_MAX_FILES

Run boot and stall limits are covered in Run liveness.

On this page