# Executions

> Code-mode execution via QuickJS, paused states for auth and approval, `execute` and `resume` HTTP routes, and MCP elicitation handling.

- Repository: RhysSullivan/executor
- GitHub: https://github.com/RhysSullivan/executor
- Human docs: https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052
- Complete Markdown: https://grok-wiki.com/public/docs/rhyssullivan-executor-564383868052/llms-full.txt

## Source Files

- `packages/core/api/src/executions/api.ts`
- `packages/core/api/src/handlers/executions.ts`
- `packages/core/execution/src/engine.ts`
- `packages/kernel/runtime-quickjs/src/index.ts`
- `packages/hosts/mcp/src/tool-server.ts`
- `apps/cli/src/main.ts`

---

---
title: "Executions"
description: "Code-mode execution via QuickJS, paused states for auth and approval, `execute` and `resume` HTTP routes, and MCP elicitation handling."
---

Executor runs agent-authored TypeScript in a sandboxed QuickJS runtime, wires a `tools.*` proxy into that sandbox, and routes every tool call through the same executor stack used by direct invocation. When a tool needs user input (OAuth, credentials, policy approval), the execution engine pauses, returns an `executionId`, and resumes on `accept`, `decline`, or `cancel`. The same pause/resume model is exposed over HTTP (`POST /api/executions`), MCP (`execute` / `resume` tools), and the CLI (`executor resume`).

## Architecture

```mermaid
flowchart TB
  subgraph surfaces["Caller surfaces"]
    MCP["MCP host<br/>execute / resume tools"]
    HTTP["HTTP API<br/>POST /api/executions"]
    CLI["CLI<br/>call / resume"]
    UI["Web UI<br/>tool run panel"]
  end

  subgraph engine["@executor-js/execution"]
    EE["createExecutionEngine"]
    Pause["executeWithPause / resume"]
    Inline["execute + onElicitation"]
  end

  subgraph runtime["Code substrate"]
    QJS["runtime-quickjs<br/>makeQuickJsExecutor"]
    DW["runtime-dynamic-worker<br/>Cloudflare Workers"]
  end

  subgraph sandbox["QuickJS sandbox"]
    Proxy["tools.* proxy"]
    Emit["emit() / console"]
    Invoker["SandboxToolInvoker"]
  end

  subgraph executor["@executor-js/sdk Executor"]
    Tools["executor.execute(address, args)"]
    Policy["policy + elicitation"]
  end

  MCP --> EE
  HTTP --> EE
  CLI --> HTTP
  UI --> HTTP
  EE --> Pause
  EE --> Inline
  EE --> QJS
  EE --> DW
  QJS --> Proxy
  Proxy --> Invoker
  Invoker --> Tools
  Tools --> Policy
```

| Layer | Package / surface | Role |
| --- | --- | --- |
| Engine | `@executor-js/execution` | Pause/resume orchestration, result formatting, built-in sandbox tools |
| Runtime | `@executor-js/runtime-quickjs` (local, self-host) or `@executor-js/runtime-dynamic-worker` (cloud) | Sandboxed JS execution with timeout and memory limits |
| Invoker | `makeExecutorToolInvoker` | Maps `tools.<path>(args)` to `executor.execute` |
| Host wiring | `@executor-js/api` `makeExecutionStack` | Binds scoped executor + `CodeExecutorProvider` per request |

<Note>
MCP paused executions are **session-scoped**. The web approval page at `/resume/:executionId` resolves pauses through `/api/mcp-sessions/:mcpSessionId/executions/:executionId`, not the session-less `/api/executions/:id` route. HTTP `POST /api/executions` uses a separate engine instance per request.
</Note>

## Execution lifecycle

```mermaid
stateDiagram-v2
  [*] --> Running: execute / executeWithPause
  Running --> Completed: script finishes
  Running --> Paused: elicitation or policy gate
  Paused --> Running: resume accept (+ content)
  Paused --> Completed: resume decline / cancel
  Paused --> Completed: sandbox timeout / error
  Completed --> [*]
```

An execution returns one of two top-level statuses:

| Status | Meaning |
| --- | --- |
| `completed` | Sandbox finished. Includes `text`, `structured`, and `isError` when the script or a tool failed inside the sandbox. |
| `paused` | Waiting for user interaction. Includes `executionId`, interaction metadata, and resume instructions in `text` / `structured`. |

Paused execution IDs are globally unique (`exec_<uuid>`). The engine keeps a bounded FIFO cache (64 entries) of settled outcomes so duplicate `resume` calls replay the same result instead of returning "not found".

## QuickJS sandbox

Local and self-hosted deployments use `makeQuickJsExecutor()` from `@executor-js/runtime-quickjs`. Cloud uses `makeDynamicWorkerExecutor()` instead; both implement the same `CodeExecutor` interface from `@executor-js/codemode-core`.

### Runtime defaults

| Option | Default | Constraint |
| --- | --- | --- |
| `timeoutMs` | `300_000` (5 minutes) | Minimum `100` |
| `memoryLimitBytes` | `64 * 1024 * 1024` | Per-VM allocation cap |
| `maxStackSizeBytes` | `1 * 1024 * 1024` | Call-stack depth cap |

### Sandbox API

Scripts run inside an async IIFE with these globals:

| Global | Behavior |
| --- | --- |
| `tools` | Lazy proxy. `await tools.github.org.main.getRepo({ owner, repo })` dispatches to the matching executor tool address. |
| `tools.search({ query, namespace?, limit?, offset? })` | Built-in tool discovery (default limit `12`). |
| `tools.describe.tool({ path })` | Returns compact TypeScript input/output shapes for a tool path. |
| `tools.executor.sources.list({ query?, limit?, offset? })` | Lists configured integrations. |
| `emit(value)` | Sends user-visible output (MCP content blocks, `ToolFile`, or text). Emitted items are not returned to the model. |
| `console.log/warn/error/info/debug` | Captured into execution logs. |
| `fetch` | **Disabled** — all network access goes through `tools.*`. |

TypeScript type syntax (`: T`, `as T`, generics) is stripped before evaluation. Decorators and `enum` are not supported.

<CodeGroup>
```typescript title="Example: search then call"
const { items } = await tools.search({ query: "github issues", limit: 12 });
const path = items[0]?.path;
if (!path) return "No matching tools found.";

const details = await tools.describe.tool({ path });
const result = await tools[path]({ title: "Bug report" });
return result;
```

```typescript title="Example: emit file to user"
const attachment = await tools.gmail.org.main.getAttachment({ id: "..." });
if (attachment.ok) emit(attachment.data);
return undefined;
```
</CodeGroup>

## Pause reasons

Executions pause when the elicitation handler is invoked mid-run. Common triggers:

| Trigger | Request type | Typical cause |
| --- | --- | --- |
| Tool `elicit()` | `FormElicitation` or `UrlElicitation` | Missing credentials, OAuth browser flow, structured user input |
| Policy `require_approval` | `FormElicitation` (empty schema) | Owner policy matched the tool path |
| Tool annotation `requiresApproval` | `FormElicitation` (empty schema) | Integration spec marked the operation as sensitive |

### Elicitation request shapes

<ParamField body="FormElicitation" type="object">
Structured input or approval-only gate.

- `message` (string): Human-readable prompt.
- `requestedSchema` (JSON Schema object): Fields to collect. An empty schema means "approve or decline" with no form fields.
</ParamField>

<ParamField body="UrlElicitation" type="object">
Browser redirect flow (OAuth, external approval page).

- `message` (string): Human-readable prompt.
- `url` (string): URL the user must open.
- `elicitationId` (string): Correlation id for the callback.
</ParamField>

### Resume response

<ParamField body="action" type="accept | decline | cancel" required>
How the user responded to the paused interaction.
</ParamField>

<ParamField body="content" type="object">
Required for `accept` on form elicitations with a non-empty `requestedSchema`. Omitted or `{}` for approval-only gates.
</ParamField>

On `decline` or `cancel`, the engine raises `ElicitationDeclinedError` and the sandbox sees a tool failure message.

## HTTP API

All routes are under `/api/executions` and use the pause/resume engine (`executeWithPause`).

| Method | Path | Handler | Purpose |
| --- | --- | --- | --- |
| `POST` | `/api/executions` | `execute` | Run code; return `completed` or `paused` |
| `GET` | `/api/executions/:executionId` | `getPaused` | Inspect a paused execution without resuming |
| `POST` | `/api/executions/:executionId/resume` | `resume` | Submit `action` + optional `content` |

:::endpoint POST /api/executions
Run TypeScript in the QuickJS sandbox. Returns immediately on completion or on the first pause point.
:::

<ParamField body="code" type="string" required>
TypeScript/JavaScript source to execute.
</ParamField>

<ResponseField name="status" type="completed | paused">
Top-level outcome discriminator.
</ResponseField>

<ResponseField name="text" type="string">
Human-readable result or pause instructions (includes `executionId` and resume guidance when paused).
</ResponseField>

<ResponseField name="structured" type="object">
Machine-readable envelope. Completed runs use `{ status: "completed" | "error", result?, error?, logs, emitted? }`. Paused runs use `{ status: "waiting_for_interaction", executionId, interaction: { kind, message, instructions, url?, requestedSchema?, address, args } }`.
</ResponseField>

<ResponseField name="isError" type="boolean">
Present on `completed` responses when the sandbox or a tool returned an error.
</ResponseField>

<RequestExample>
```bash
curl -sS -X POST http://localhost:3001/api/executions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"code":"return await tools.search({ query: \"github\" });"}'
```
</RequestExample>

<ResponseExample>
```json
{
  "status": "completed",
  "text": "{\n  \"items\": [...],\n  \"total\": 3,\n  \"hasMore\": false\n}",
  "structured": {
    "status": "completed",
    "result": { "items": [], "total": 3, "hasMore": false },
    "logs": []
  },
  "isError": false
}
```
</ResponseExample>

:::endpoint POST /api/executions/:executionId/resume
Resume a paused execution. May return another `paused` status if a second elicitation occurs in the same script.
:::

<ParamField body="executionId" type="string" required>
ID from the paused response (`structured.executionId` or `text`).
</ParamField>

<ParamField body="action" type="accept | decline | cancel" required>
User decision.
</ParamField>

<ParamField body="content" type="object">
JSON object matching `requestedSchema` when accepting a form elicitation.
</ParamField>

<ResponseField name="status" type="completed | paused">
Same union as `POST /api/executions`.
</ResponseField>

<RequestExample>
```bash
curl -sS -X POST "http://localhost:3001/api/executions/exec_abc123/resume" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"action":"accept","content":{"note":"Approved"}}'
```
</RequestExample>

:::endpoint GET /api/executions/:executionId
Fetch formatted pause details for a known `executionId`. Returns `404 ExecutionNotFoundError` if the id is unknown or already resumed.
:::

## MCP surface

The MCP host registers two tools when elicitation is not fully native:

| Tool | When registered | Input |
| --- | --- | --- |
| `execute` | Always | `{ code: string }` |
| `resume` | `elicitationMode` is `model` or `browser` | See mode below |

### Elicitation modes

<Tabs>
<Tab title="model (default)">
The host calls `engine.executeWithPause(code)`. On pause, the model receives structured interaction metadata and must call `resume` with the user's decision.

`resume` input:
- `executionId` (string)
- `action`: `accept` | `decline` | `cancel`
- `content` (optional JSON string, default `"{}"`)

This is the default for `executor mcp --elicitation-mode model`.
</Tab>

<Tab title="browser">
Same pause flow, but `execute` returns `status: "user_approval_required"` with an `approvalUrl` instead of full interaction details. The model prompts the user to open the URL; `resume` blocks until the browser records a decision (up to 10 minutes).

Local CLI builds approval URLs as `{baseUrl}/resume/{executionId}?mcp_session_id=...`.

`executor mcp --elicitation-mode browser` selects this mode.
</Tab>

<Tab title="native">
The host calls `engine.execute` with an inline MCP `elicitInput` handler. Paused executions never surface `executionId`; the MCP client handles form/url elicitation directly. The `resume` tool is not registered.

Used when the connected MCP client advertises elicitation capabilities and the host opts into native mode.
</Tab>
</Tabs>

<Warning>
Paused MCP executions live in the MCP session's in-memory engine. They expire after a few minutes or when the session/runtime restarts. A missing execution returns `status: "execution_not_found"` with recovery guidance to re-run `execute`.
</Warning>

## CLI

The CLI routes codemode through the HTTP API.

<Steps>
<Step title="Run code or a tool path">
`executor call` and `executor tools search` compile to sandbox code and call `POST /api/executions`.
</Step>

<Step title="Handle a pause">
On `status: "paused"`, the CLI prints pause text, the browser approval URL (`{origin}/resume/{executionId}`), and ready-made `executor resume` commands.
</Step>

<Step title="Resume from the terminal">
```bash
executor resume --execution-id exec_abc123 --action accept
executor resume --execution-id exec_abc123 --action accept --content '{"note":"ok"}'
executor resume --execution-id exec_abc123 --action decline
```
</Step>
</Steps>

Use `--base-url` or `--server` to target a remote Executor instance. Paused state is not persisted across server restarts.

## Result formatting

`formatExecuteResult` and `formatPausedExecution` normalize engine output for HTTP and MCP:

| Field | Completed | Paused |
| --- | --- | --- |
| `text` | Truncated at 30,000 chars. Includes result value, logs, or emit acknowledgment. | Pause message, URL or schema, `executionId`, and step-by-step resume instructions. |
| `structured.status` | `"completed"` or `"error"` | `"waiting_for_interaction"` |
| `isError` | `true` when sandbox `error` is set | Not set (pause is not an error) |

Scripts that only `emit()` without `return` produce `(no return value; N items emitted to the user)` in `text` and an `emitted` count in `structured`.

## SDK embedding

```typescript
import { createExecutor } from "@executor-js/sdk";
import { createExecutionEngine } from "@executor-js/execution";
import { makeQuickJsExecutor } from "@executor-js/runtime-quickjs";

const executor = await createExecutor({ onElicitation: "accept-all" });

const engine = createExecutionEngine({
  executor,
  codeExecutor: makeQuickJsExecutor({ timeoutMs: 120_000 }),
});

// Inline elicitation (MCP-native style)
const result = await engine.execute(code, {
  onElicitation: async (ctx) => {
    // render ctx.request, collect user input
    return { action: "accept", content: { field: "value" } };
  },
});

// Pause/resume (HTTP / model-MCP style)
const started = await engine.executeWithPause(code);
if (started.status === "paused") {
  const resumed = await engine.resume(started.execution.id, {
    action: "accept",
    content: { note: "approved" },
  });
}
```

`engine.getDescription` returns the canonical workflow prose (search → describe → call) plus up to 50 connection prefixes for LLM tool selection.

## Failure modes

| Symptom | Likely cause | Recovery |
| --- | --- | --- |
| `ExecutionNotFoundError` (404) | Pause expired, already resumed, or wrong API scope (MCP session vs HTTP) | Re-run `execute`; for MCP pauses, include `mcp_session_id` on `/resume` |
| `No paused execution: exec_...` (MCP) | Session restarted or timeout | Re-run `execute` with the original code |
| `fetch is disabled in QuickJS executor` | Script called `fetch` directly | Route through `tools.*` |
| QuickJS timeout after N ms | Script or pending tool calls exceeded `timeoutMs` | Increase `timeoutMs` or shorten the workflow |
| `Tool requires approval but ... declined` | User chose `decline` or `cancel` on a policy or elicitation gate | Adjust policy or re-run with approval |
| Hang until client timeout | Rare race on fast sandbox failure (fixed via `Effect.raceFirst`) | Upgrade to current engine; verify runtime returns failures promptly |

Set `EXECUTOR_MCP_DEBUG=1` to log MCP elicitation capability snapshots and pause/resume transitions.

## Related pages

<CardGroup>
<Card title="Tools" href="/tools">
Tool addresses, discovery, and how the sandbox `tools` proxy maps paths to executor tools.
</Card>

<Card title="Policies" href="/policies">
Per-tool `require_approval` rules that trigger approval pauses during execution.
</Card>

<Card title="Manage policies" href="/manage-policies">
Handle approval pauses from the web UI, MCP, and CLI resume flow.
</Card>

<Card title="MCP proxy" href="/mcp-proxy">
How Executor exposes `execute` and `resume` as MCP tools in front of integrations.
</Card>

<Card title="HTTP API reference" href="/http-api-reference">
Full route inventory including `executions`, payloads, and error shapes.
</Card>

<Card title="CLI reference" href="/cli-reference">
`executor call`, `executor resume`, and `executor mcp --elicitation-mode` flags.
</Card>

<Card title="Quickstart" href="/quickstart">
End-to-end path from install through a paused execution and resume.
</Card>
</CardGroup>
