Executions

An execution is a single run of a skill with specific input. Executions are processed asynchronously and stream results in real-time via Server-Sent Events backed by a durable, replayable event stream.

One-At-A-Time Rule: Each user can only have one active execution at a time, across all their workspaces. Attempting to start a second execution while one is working returns EXECUTION_RUNNING. For multi-step pipelines, use Workflows.

Execution Lifecycle

┌────────┐    user.message event    ┌─────────┐    agent finishes    ┌───────────┐
│  idle  │ ─────────────────────>  │ working │ ──────────────────>  │ completed │
└────────┘                         └─────────┘                       └───────────┘
                                        │
                                   user cancel / error
                                        │
                                   ┌────────┐
                                   │ failed │  (stop_reason: error or interrupt)
                                   └────────┘

States

StateDescription
idleCreated, waiting for a user.message event to trigger the run
runningActively processing, streaming agent output
completedSuccessfully finished, artifacts saved
failedError occurred during execution
cancelledCancelled before completion

Starting an Execution

Skills are not executed over MCP — there is no execute-skill tool anywhere any more. Start runs through the REST API below. Over MCP you discover, install, and monitor skills (get-execution-status, list-executions, cancel-execution).

An execution is what the REST API creates. Asking an agent in chat to run a skill does something different: the agent carries the skill out in its own workspace sandbox, as part of the conversation. That produces no execution and so appears nowhere on this page — look at the thread, or at any artifact the agent saved.

Via REST API — one shot

The simplest path runs a skill in a single request:

curl -X POST https://next.clanker.engineer/api/v1/skills/readme-generator/run \
  -H "Content-Type: application/json" \
  -H "x-api-key: ck_live_xxxxx" \
  -d '{ "input": "Create a README for my TypeScript CLI tool" }'

Response:

{
  "executionId": "exec_abc123",
  "skillName": "README Generator",
  "status": "started",
  "message": "Execution started. Connect to SSE stream for real-time updates.",
  "streamTokenUrl": "/api/v1/executions/exec_abc123/stream-token"
}

Via REST API (Sessions API — three steps)

The Sessions API pattern (compatible with managed-agent providers) lets you subscribe to the stream before the run begins.

Step 1 — Create the execution (both skill_slug and a non-empty input are required):

curl -X POST https://next.clanker.engineer/api/v1/executions \
  -H "Content-Type: application/json" \
  -H "x-api-key: ck_live_xxxxx" \
  -d '{ "skill_slug": "readme-generator", "input": "Create a README for my TypeScript CLI tool" }'

Response: { "id": "exec_abc123", "skill_slug": "readme-generator", "skill_name": "README Generator", "status": "idle", "streamTokenUrl": "/api/v1/executions/exec_abc123/stream-token" }

Step 2 — Mint a signed stream URL, then connect to it (before sending input):

GET /api/v1/executions/exec_abc123/stream-token
→ { "sseUrl": "https://sandbox.clanker.engineer/executions/exec_abc123/events?ts=…&sig=…" }

Connect an EventSource to that sseUrl. It is short-lived and ownership-checked, so re-mint it on every reconnect. The stream is served by the sandbox worker, not by this API.

Step 3 — Send user.message to start the run:

curl -X POST https://next.clanker.engineer/api/v1/executions/exec_abc123/events \
  -H "Content-Type: application/json" \
  -H "x-api-key: ck_live_xxxxx" \
  -d '{
    "events": [
      {
        "type": "user.message",
        "content": [{ "type": "text", "text": "Create a README for my TypeScript CLI tool" }]
      }
    ]
  }'

See Executions API for the full endpoint reference and session-level stream details.

Streaming Results

Executions stream results via Server-Sent Events on a durable, replayable endpoint served by the sandbox worker. Mint its signed URL first — this API exposes no GET stream of its own:

GET /api/v1/executions/:executionId/stream-token
→ { "sseUrl": "…/executions/:executionId/events?ts=…&sig=…" }

POST /api/v1/executions/:executionId/events is a different thing entirely: it SENDS user.message / user.interrupt INTO a run. It accepts no GET.

The stream replays all history from the beginning (gap-free), then delivers live events. It closes automatically when session.status_idle arrives.

Event Types

TypeDescriptionKey Payload Fields
session.status_runningExecution started running—
session.status_idleExecution endedstop_reason: { type }
agent.messageText output from the agentcontent: [{ type, text }]
agent.tool_useTool call in progressname, tool, label
agent.thinkingExtended thinking block (internal)—
agent.infoInformational statusvaries
execution.resultFinal result summarysummary, cost, tokenUsage
span.model_request_endToken usage per model callusage: { input_tokens, output_tokens }

Stop reason types (inside session.status_idle):

TypeDescription
end_turnAgent finished normally
interruptUser cancelled
limit_reachedExecution credit limit hit
errorExecution failed

SSE Client Example

// 1. Mint the signed stream URL (authenticated, ownership-checked).
const { sseUrl } = await fetch(
  `https://next.clanker.engineer/api/v1/executions/${executionId}/stream-token`,
  { headers: { 'x-api-key': 'ck_live_xxxxx' } }
).then((r) => r.json());

// 2. Connect to it. The signature is IN the URL, so no auth header is needed
//    (and EventSource cannot send one anyway).
const eventSource = new EventSource(sseUrl);

eventSource.onmessage = (event) => {
  const data = JSON.parse(event.data);

  if (data.type === 'agent.message') {
    for (const block of data.content) {
      if (block.type === 'text') process.stdout.write(block.text);
    }
  }

  if (data.type === 'agent.tool_use') {
    console.log('Using tool:', data.label);
  }

  if (data.type === 'session.status_idle') {
    console.log('Done. Stop reason:', data.stop_reason?.type);
    eventSource.close();
  }
};

Checking Execution Status

Via MCP

{
  "tool": "get-execution-status",
  "arguments": {}
}

Response (working):

{
  "success": true,
  "data": {
    "executionId": "exec_abc123",
    "status": "working",
    "skillSlug": "readme-generator",
    "startedAt": "2024-01-15T10:30:00Z"
  }
}

Response (idle):

{
  "success": true,
  "data": {
    "status": "idle",
    "message": "No active executions"
  }
}

Via REST API

GET /api/v1/executions/:executionId/status

Repository Runs

When an execution is started against a repository (a GitHub connector with a repo and branch), the sandbox mounts the branch as a lazily-hydrated worktree instead of cloning it, so the run starts in seconds. The sandbox holds no GitHub token and no provider key of any kind: model calls, connector actions and git traffic all go through the platform, which attaches the credential on its own side.

Execution Limits

LimitValue
Concurrent executions1 per user
Maximum duration60 minutes (the sandbox lifetime)
Rate limit100 requests/minute

The limit follows the person, not the workspace: being in several workspaces does not give you a second concurrent run. Each workspace also has a share of the platform’s sandbox capacity, so a busy workspace can be asked to retry.

Execution Cost

Each execution consumes Dollarinos (Ds), at the rate for the model that served each call. The fallback rate for a model with no rate of its own is:

ComponentCost
Input tokens1 Ds per 1,000 tokens
Output tokens5 Ds per 1,000 tokens
Minimum balance to start10 Ds

Usage is metered from the token counts the model provider reports, not from what the skill reports. A failed, cancelled or timed-out execution is still charged for the model calls it made before it ended; only BYOK runs and runs that made no model calls are free. See Credits.

The final cost appears in the execution.result event:

{
  "summary": "Generated README.md with 8 sections",
  "cost": 45,
  "tokenUsage": { "input": 1500, "output": 8000 }
}

List Executions

Via MCP

{
  "tool": "list-executions",
  "arguments": { "limit": 10 }
}

Via REST API

GET /api/v1/executions

Rerunning Executions

Any completed execution can be rerun with new instructions:

  • Rerun creates a new output (new artifact)
  • Original outputs remain unchanged

Parent-Child Execution Linking

┌──────────────────┐         ┌─────────────────┐
│ Parent Execution │ ──────> │ Child Execution  │
│ (original)       │         │ (rerun)          │
└──────────────────┘         └─────────────────┘
  • Each execution can have at most one child
  • If you rerun a child, it replaces the previous rerun
  • Both parent and child are visible in execution detail view

Error Handling

Common execution errors:

ErrorCauseSolution
INSUFFICIENT_CREDITSNot enough DsPurchase credit pack
EXECUTION_RUNNINGAnother execution activeWait or cancel first
SKILL_NOT_FOUNDSkill not installedInstall the skill first
RATE_LIMITEDToo many requestsWait and retry
TIMEOUTExecution too longSimplify input or split task

Best Practices

  1. Subscribe before sending input — open the SSE stream (step 2) before posting user.message (step 3) to avoid missing early events
  2. Use SSE instead of polling — polling the status endpoint is fine for simple checks, but SSE is more efficient for real-time output
  3. Handle session.status_idle — always close EventSource on this event
  4. Reconnect with lastSeq — on reconnect, pass the last seen sequence number (&lastSeq=<n> on the signed URL) to skip already-rendered events
  5. Keep inputs focused — smaller, specific inputs execute faster and cost less