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.
POST /v1/runs is the native way to drive the Automate assistant. One request, one task, one blocking response containing everything the run did.
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
continuationfor follow-ups instead. - Say what "done" looks like when the output has to be machine-readable — or better, use
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.
{
"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.
{
"prompt": "How many PDF files are in my Documents folder?",
"answerContract": "Reply with exactly one line: FINAL ANSWER: <integer>"
}
{
"status": "completed",
"answer": "I counted the PDFs in your Documents folder — there are 14.\n\nFINAL ANSWER: 14",
"finalAnswer": "14"
}
answerstays exactly what a person would have seen. Nothing is stripped.finalAnsweris the text after the lastFINAL 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,
finalAnsweris the whole answer — so always handle the case where it is prose rather than your expected shape. finalAnsweris present only when you sent ananswerContract.
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:
{
"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:
{ "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
{ "prompt": "Read input.csv and write a summary.md next to it.", "restrictToWorkspace": true }
Three rules worth internalizing:
- 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.
- 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.
- 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.
// 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. The fields below select from what the user has configured.
To force specific ones into a run:
{
"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:
{
"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) orstring[](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.
{
"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:
{ "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:
{
"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
{ "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
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
{
"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
taskIdso 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.