# Running Automate Tasks

> The /v1/runs contract in depth — prompts, answer contracts, workspace scoping, continuations, AI Skills and MCP selection, human-in-the-loop answers, destructive-tool approvals, and how to read steps, traces, and token usage.

Source: https://developers.cephable.com/docs/automate-http-server/runs

---
`POST /v1/runs` is the native way to drive the Automate assistant. One request, one task, one blocking response containing everything the run did.

```bash
curl -sS http://127.0.0.1:4317/v1/runs \
  -H "Authorization: Bearer $CEPHABLE_AUTOMATE_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "prompt": "Find the invoice PDFs in my Documents folder from this month and list their totals." }'
```

---

## Writing a good prompt

The `prompt` is what the agent runs. It reaches the same system prompt, tool set, and middleware as a prompt typed into the AI Workflows panel, so the guidance is the same as it is for a person:

- **Name the outcome, not the tool.** "Draft a reply to the newest email from Dana" works better than "call draft_email".
- **Anchor the scope.** Folders, filenames, apps, and date ranges cut down exploratory tool calls, which are the expensive part of a run.
- **One task per run.** Long compound instructions on a small on-device model drift. Use `continuation` for follow-ups instead.
- **Say what "done" looks like** when the output has to be machine-readable — or better, use [`answerContract`](#answercontract).

### Advisory preferences: `additionalWorkflowPrompt`

Appended to the system prompt as user preferences. Advisory, not binding — the agent may deviate if the task calls for it. Use it for style and standing conventions, not for output format contracts.

```jsonc
{
  "prompt": "Summarize the meeting notes in ~/notes/2026-09-14.md",
  "additionalWorkflowPrompt": "Always use metric units. Prefer British spelling. Never open a browser."
}
```

### Reasoning budget: `thinkingLevel`

`"low" | "medium" | "high" | "max"`, applied per run — so you can spend more on a hard task without changing the user's setting or re-warming the worker. Omit it to use whatever the user has configured.

Higher levels cost latency roughly in proportion to the extra reasoning tokens. For short, mechanical tasks `low` is usually indistinguishable from `high` in quality and noticeably faster.

---

## `answerContract`

The single most useful option for programmatic callers. It declares a **binding** shape for the closing message, rendered as its own final system-prompt section so it outranks the assistant's usual "short and friendly" guidance.

```jsonc
{
  "prompt": "How many PDF files are in my Documents folder?",
  "answerContract": "Reply with exactly one line: FINAL ANSWER: <integer>"
}
```

```jsonc
{
  "status": "completed",
  "answer": "I counted the PDFs in your Documents folder — there are 14.\n\nFINAL ANSWER: 14",
  "finalAnswer": "14"
}
```

- `answer` stays **exactly** what a person would have seen. Nothing is stripped.
- `finalAnswer` is the text after the **last** `FINAL ANSWER:` marker in the answer. The last one, deliberately: a model satisfying a contract often quotes the marker once while explaining itself before emitting the real value.
- If the model never emits the marker, `finalAnswer` is the whole answer — so always handle the case where it is prose rather than your expected shape.
- `finalAnswer` is present **only** when you sent an `answerContract`.

Nothing in the Cephable app sets this option, so it cannot change the product experience for the user.

**Design your contract around the marker.** The extraction looks for `FINAL ANSWER:` (case-insensitive) and nothing else, so contracts that ask for JSON should still route it through the marker:

```jsonc
{
  "answerContract": "End your reply with a single line: FINAL ANSWER: {\"count\": <integer>, \"largest\": \"<filename>\"}"
}
```

Then `JSON.parse(result.finalAnswer)` — with a fallback, because a small local model will occasionally wrap it in a code fence.

---

## `include` — trimming the response

All three diagnostic blocks are returned by default. Turn off what you do not read:

```jsonc
{ "prompt": "…", "include": { "steps": true, "trace": false, "events": false } }
```

| Block | Size | Keep it when |
|---|---|---|
| `steps` | Moderate | You want to know what the agent actually did — almost always worth keeping |
| `trace` | Large | You are debugging model behavior or prompt construction |
| `events` | **Very large** | You are debugging the channel itself. It rebroadcasts the whole step list on every update, so a long research run's event stream alone can be hundreds of kilobytes |

For production integrations, `{ "trace": false, "events": false }` is the sensible default.

---

## `restrictToWorkspace` — filesystem scope

**Default: `false`.** By default an API run reaches the same files a run started from the app does. That is deliberate: the caller holds the access key, and the user enabled this server on purpose, so an API run behaves like the user's own run.

Set it to `true` to confine the run's file and CLI tools to the server's workspace folder — the `workspace` path reported by `/health`:

```
Windows  %APPDATA%\Cephable\automate-http-workspace
macOS    ~/Library/Application Support/Cephable/automate-http-workspace
Linux    ~/.config/Cephable/automate-http-workspace
```

```jsonc
{ "prompt": "Read input.csv and write a summary.md next to it.", "restrictToWorkspace": true }
```

Three rules worth internalizing:

1. **It can only narrow.** Where the user has configured their own workspace restriction in Cephable, theirs wins and this is ignored. An API caller can never widen or relocate the user's sandbox.
2. **It applies to that run alone.** Cephable sets the scope at the start of every run — from the request where one asks, from the user's settings otherwise — so an API run can never leave the assistant sandboxed for the user's next run from the panel.
3. **Staging plus diffing is the pattern.** Write your inputs into the workspace folder before the run, then diff the folder afterwards to find what the task produced. This is exactly how the internal benchmark harness detects task output.

Turn it on for anything unattended, batch, or test-shaped. Leave it off for an integration that is genuinely acting on the user's behalf across their own files.

---

## `continuation` — follow-up turns

`continuation: true` continues the previous run's conversation instead of starting a new one, mirroring the app's own follow-up path. The agent can refer to what the last turn produced.

```jsonc
// turn 1
{ "prompt": "Summarize ~/reports/q3.md" }
// turn 2
{ "prompt": "Now turn that into five bullets for a slide.", "continuation": true }
```

- There is no conversation id to manage. "Previous" means the assistant's most recent run — which includes runs the *user* started in the panel.
- A warm worker keeps the checkpointed thread and the KV cache, so a continuation is fast. If the worker was torn down in between, Cephable seeds the thread from stored history instead.
- Because the conversation is shared with the app, do not assume your previous turn is still the last one in a multi-user or long-idle scenario. For strict isolation, make every run self-contained and skip `continuation`.

---

## AI Skills and MCP servers

The user's installed **AI Skills** and configured **MCP servers** are available to API runs. Omit both fields and Cephable does what it does in the app: a per-run pre-selection step picks a budget-capped set of skills and MCP servers by relevance to the prompt, then exposes their names and descriptions, letting the agent read a skill's `SKILL.md` on demand.

To add tools of your **own** — an MCP server you host, or functions your process executes — see [Custom tools](/docs/automate-http-server/custom-tools). The fields below select from what the *user* has configured.

To force specific ones into a run:

```jsonc
{
  "prompt": "File this expense report following our process.",
  "selectedSkillIds": ["expense-report-process"],
  "selectedMcpServerIds": ["internal-finance-mcp"]
}
```

- Explicitly selected items are **always** exposed for the run; Cephable fills any remaining budget with relevance-picked ones.
- There is no endpoint that lists skill or MCP ids. Get them from the Cephable app (**Extensions → AI Skills**, and the MCP/Tools configuration) and treat them as configuration in your integration, not as something to discover at runtime.
- MCP connections are resolved in the main process — secrets injected, OAuth tokens refreshed — before the worker connects. Your request never carries credentials.
- Whether the agent may run a skill's scripts is the **user's** setting, not a request option.

---

## Human-in-the-loop questions

When the agent calls `request_user_input`, a run started from the app shows the user a form. An API run has no one to ask, so the server answers from `hitlAnswers`:

```jsonc
{
  "prompt": "Draft the weekly status email to the team.",
  "hitlAnswers": {
    "recipient": "team@example.com",
    "tone": "concise",
    "highlights": ["shipped the importer", "fixed the sync bug"]
  }
}
```

- Keys are **field ids**, and values are a `string` (single answer) or `string[]` (multi-select).
- If the question includes a **required** field you did not supply, the server **cancels the run** rather than hanging. The response comes back with `status: "canceled"`.
- Extra keys that the question does not ask for are ignored.

Field ids come from the prompt and skill that raise the question, so you generally learn them by running the task once and reading the `steps` for the `request_user_input` call. A more robust pattern for unattended runs: write prompts that do not need to ask — state the recipient, the tone, and the constraints up front.

---

## Destructive tools

`delete_path`, `run_command`, and `move_path` pause the run for approve/edit/reject review. The server's policy:

| `allowDestructiveTools` | Behavior |
|---|---|
| `false` (default) | Every request is **rejected**, with a message the agent sees: *"Rejected by Automate HTTP server policy…"*. The run continues and usually adapts |
| `true` | Every request is **approved** as-is |

There is no per-tool or per-call granularity, and no way to inspect the pending call before deciding — the decision is made from the request body before the run starts.

Because `run_command` is in that set, `allowDestructiveTools: true` grants the caller arbitrary local command execution through the agent. Only combine it with `restrictToWorkspace: true` and a sandboxed profile. See [Security model](/docs/automate-http-server/security).

```jsonc
{
  "prompt": "Convert every .wav in the workspace to .mp3 and delete the originals.",
  "restrictToWorkspace": true,
  "allowDestructiveTools": true
}
```

---

## Timeouts

`timeoutMs` defaults to **900000** (15 minutes) and must be at least `1000`.

On timeout, the server cancels the run and the HTTP request fails with `400`:

```jsonc
{ "error": { "message": "Automate run timed out after 600000ms", "type": "invalid_request_error" } }
```

Note what you *lose*: a timeout returns an error envelope, **not** a run record, so there are no `steps` or `usage` for the attempt. If diagnostics on slow runs matter to you, set a generous `timeoutMs` and enforce your own deadline by calling `POST /v1/automate/cancel` instead — a cancelled run settles as a full record with `status: "canceled"`.

Set your **client** timeout above `timeoutMs`. Many HTTP clients default to 30–100 seconds, which is far below a realistic agent run; a client-side abort leaves the run going in the app.

---

## Reading the response

### `steps`

The ordered list of what the agent did. One entry per tool call:

```jsonc
{
  "id": "step-3",
  "index": 3,
  "title": "Write a user's file",
  "status": "success",
  "toolName": "write_user_file",
  "toolArgs": { "path": "~/reports/summary.md", "content": "…" },
  "thinking": "The user asked for a file, so I'll write the generated summary…",
  "resultSummary": "Wrote 1.4 KB to C:\\Users\\you\\reports\\summary.md",
  "producedFilePath": "C:\\Users\\you\\reports\\summary.md",
  "producedIsDirectory": false,
  "producedContent": "# Summary\n…",
  "producedContentId": "g1",
  "consumedContentIds": ["s2"]
}
```

| Field | Use it for |
|---|---|
| `status` | `pending` · `running` · `success` · `failed` · `skipped` |
| `toolName` | Asserting *how* a task was accomplished, not just that it was |
| `toolArgs` | The exact arguments — the best debugging signal when a run goes sideways |
| `thinking` | The model's stated reasoning for the step |
| `resultSummary` | A short summary of the tool result |
| `producedFilePath` | The **resolved absolute path** of a file or folder created or edited. Use this rather than the path in `toolArgs`, which may be relative or fuzzy-matched |
| `producedContent` | The **full** text a content tool generated. The model only ever sees a bounded preview, so this is the only complete copy |
| `producedContentId` / `consumedContentIds` | Chaining ids (`g1`, `s2`…) linking a producing step to the step that consumed it |

For an integration that needs the artifact rather than the prose, reading `producedFilePath` or `producedContent` off the last relevant step is more reliable than parsing `answer`.

### `usage`

```jsonc
{ "inputTokens": 5120, "outputTokens": 344, "generationMs": 8345, "ttftMs": 610, "tps": 41.2 }
```

`null` when no metrics were emitted. `durationMs` on the record is wall clock for the whole run — including model load, cold start, and tool execution — so it is normally much larger than `generationMs`.

### `trace` and `events`

`trace` is the raw model message list (system, human, AI, and tool messages) — the ground truth for prompt-construction and tool-calling problems. `events` is every channel event the run emitted, in order; useful for reconstructing timing and intermediate state, and large enough that you should exclude it unless you are actively using it.

### Status and outcome

```javascript
if (body.schemaVersion !== 1) throw new Error(body.error.message); // request never ran
switch (body.status) {
    case 'completed':  return body.finalAnswer ?? body.answer;
    case 'canceled':   /* you, the user, or a timeout stopped it */ break;
    case 'terminated': /* the worker was killed — cold start next run */ break;
    case 'failed':     /* inspect body.errorCode and the last failed step */ break;
}
```

`HTTP 500` accompanies every non-`completed` status. It is not a server fault — the body is a complete run record.

---

## A complete, well-behaved run request

```jsonc
{
  "taskId": "nightly-report-2026-09-14",
  "prompt": "Read every .csv in the workspace, total the Amount column per file, and write results.md summarizing them.",
  "timeoutMs": 600000,
  "thinkingLevel": "medium",
  "answerContract": "End with FINAL ANSWER: <total across all files, as a number>",
  "restrictToWorkspace": true,
  "allowDestructiveTools": false,
  "include": { "steps": true, "trace": false, "events": false }
}
```

- A `taskId` so the record correlates with your own logs.
- A scoped prompt with a named output artifact.
- A contract, so the number is machine-readable without parsing prose.
- Workspace confinement, because it is unattended.
- Destructive tools off.
- Diagnostics trimmed to the block that is actually read.
