Dispatch tasks to your ThinkForce AI agents via REST API and poll for results easily without server setup or complex configuration.
# ThinkForce Missions API — Skill for AI Agents
You are operating an instance of ThinkForce, a multi-agent orchestration platform. This skill teaches you how to **manage Missions and Subtasks (steps)** on behalf of a user. Read all sections before acting; follow the decision rules in section 14.
Base URL: `https://app.thinkforce.ai`. Every request needs the header `X-TF-API-Key: <user_api_key>`. The user's `companyId` must be in every request body (and as a query param on GETs).
---
## 0. Bootstrap — discovering the user's `companyId`
You almost never receive `companyId` upfront. Resolve it once at session start, cache it, and reuse on every subsequent call.
### Primary method — `GET /api/companies`
The API key encodes which company you're operating on. Hit this endpoint first:
```http
GET /api/companies
X-TF-API-Key: <key>
```
Response:
```json
{
"companyId": "abc123",
"name": "ABC Luxury Car Service",
"status": "active",
"industry": "transportation",
"goal": "...",
"agentCount": 7
}
```
`companyId` here is what you pass on every other request. If this call returns 401, the key is invalid — stop and tell the user.
### Bootstrap pattern (run this first, every session)
```
1. GET /api/companies → save { companyId, name, agentCount }
2. (optional) GET /api/missions?companyId=<id> → see what missions already exist
3. (optional) GET /api/companies/<id>/agents → see which agents you can assign
4. Now you're ready to create / decompose / run missions.
```
### What NOT to do
- ❌ Don't ask the user for their `companyId` — the API key already binds you to one.
- ❌ Don't guess or hard-code it.
- ❌ Don't call `GET /api/companies` on every action — cache the result for the session.
- ❌ Don't try to switch companies mid-session by passing a different `companyId` — the key won't authenticate against a different company and you'll get 401.
---
## 1. Mental model
- **Mission** = a project. Has a title, description, status, priority, optional budget, optional schedule.
- **Subtask** (a.k.a. **Step**) = a unit of work inside a mission. Has an assigned agent, a status, optional `runInstructions`, and dependency edges.
- Subtasks form a **DAG** via `dependsOn[]` (upstream) and `nextSubtaskIds[]` (downstream). When a subtask completes, its `nextSubtaskIds` are auto-chained.
- Each subtask can override the agent's default toolbelt, connectors, skills, and files **just for that step**.
- Missions can be scheduled (cron) or triggered by a webhook hitting a specific subtask.
- Missions can be **shared** (clone) or **invited** (real-time collab via Y.js).
You manage state by calling REST endpoints. Never assume state — always GET fresh data before deciding.
---
## 1.5. Finding and assigning agents
Every subtask needs an `assignedAgentId` before it can run. `POST /api/missions/<id>/subtasks/<sid>/run` returns HTTP 400 (`"Assign an agent before starting this step"`) without one. This section tells you how to discover agents and pick the right one.
### List the company's agents
```http
POST /api/agents
X-TF-API-Key: <key>
Content-Type: application/json
{ "action": "list", "companyId": "<id>" }
```
Response:
```json
{
"success": true,
"total": 5,
"agents": [
{
"id": "agt_abc123",
"agentName": "Acme CEO",
"agentRole": "You are the CEO. You plan missions, coordinate other agents, and review final outputs...",
"agentType": "office",
"model": "claude-opus-4-7",
"provider": "anthropic",
"reasoningEffort": "high",
"enabledTools": ["function-Web_Search", "function-Code_Assistant", "function-Memory_Manager"],
"selectedTools": [{ "type": "function", "function": { "name": "Web_Search", "description": "...", "parameters": {...} } }, ...],
"mcpConnections": [],
"officeState": "idle",
"workspaceSync": null,
"createdAt": "...",
"updatedAt": "..."
}
],
"message": "Agents retrieved successfully"
}
```
Note: the response is wrapped (`{ success, agents, total, message }`), NOT a bare array. Read `data.agents`.
### Real agent schema (what's actually there)
| Field | What it is |
| ----------------- | ------------------------------------------------------------------------------------------------ |
| `agentName` | Short display name (e.g. "Market Researcher", "Driver Recruitment Agent"). The primary handle. |
| `agentRole` | **Free-text system prompt** for the agent — often paragraphs long. NOT a short role label. |
| `agentType` | Internal type marker (`office` for normal in-office agents, often `null` for legacy/custom). |
| `model` | Model id the agent runs on (`claude-opus-4-7`, `deepseek/deepseek-v4-flash`, etc.). |
| `enabledTools[]` | Array of strings like `"function-Web_Search"` — which tools are toggled on for this agent. |
| `selectedTools[]` | The full OpenAI-style tool objects (mirrors `enabledTools` but with definitions). Use for inspection only — for matching, prefer `enabledTools`. |
| `mcpConnections[]`| Array of MCP connector ids the agent has access to. |
| `officeState` | UI animation state: `idle | working | researching | syncing`. Not a routing signal. |
| `provider` | LLM provider key (`anthropic`, `openrouter`, `deepseek`, ...). |
**Fields that DO NOT exist on agents** (don't try to match on them): `name`, `description`, `tags`, `capabilities`, `tools`, `status`, `config`. Older docs reference these — they were never on the schema.
### What to match on
When you need to assign an agent to a subtask, match in this order:
1. **`agentName`** — the only short, human-meaningful label. Match by substring: `"researcher"`, `"designer"`, `"copywriter"`, `"marketer"`. This is your primary signal.
2. **`agentRole` substring** — grep the system prompt for domain keywords (`"video"`, `"copy"`, `"frontend"`, `"recruit"`, `"compliance"`). Slower than `agentName` matching but catches agents whose name is generic.
3. **`enabledTools[]`** — if the subtask needs `Design_Agent` to run, prefer an agent that already has `"function-Design_Agent"` in `enabledTools`. Otherwise you'll need to override with `attachedToolNames` on the subtask (which works, but is an extra step).
4. **`model`** — only relevant if the user explicitly asked for "the Claude agent" or "the fast one"; otherwise ignore.
There is no `status` field — every agent the API returns is dispatchable. There is no `inactive` or `archived` state.
### Decompose does NOT auto-assign
When you call `POST /api/missions/<id>/decompose`, the planner returns subtasks shaped like:
```json
[
{ "title": "Research top 5 competitors", "workstationKey": "researching" },
{ "title": "Draft content calendar", "workstationKey": "working" }
]
```
Each subtask comes back **without** an `assignedAgentId`. The runner refuses to start unassigned subtasks (`status === 'queued'`), so you must PATCH one before calling `/run`.
**`workstationKey` is NOT a role hint.** It's the office UI's animation/cubicle assignment for showing the agent avatar in the right workstation while the step runs (`working | researching | syncing | error`). The decomposer LLM picks one of those four buckets per subtask, but it carries no signal about which *agent* should run the step. Pick agents by `agentName` + `agentRole` substring, not by `workstationKey`.
### Assign an agent to a subtask
```http
PATCH /api/missions/<id>/subtasks/<sid>
{
"companyId": "<id>",
"assignedAgentId": "agt_def456"
}
```
That's it. Once assigned, the subtask flips `queued → assigned` and is ready for `/run`.
### Reassigning mid-mission
You can change `assignedAgentId` at any time *unless* the subtask is currently `in_progress` (in which case stop it first via the pause/cancel endpoint, then reassign and re-run). Reassigning a `done` step has no effect — the output is already cached.
### Coordinator agent fallback
If the user doesn't pick a planner for auto-decompose, the system uses `mission.coordinatorAgentId` — and that defaults to the company's CEO agent (the agent created first during onboarding, with `agentRole: "CEO"`). The CEO is always present, so you can always fall back to it when no other agent fits.
### Recipe — assign agents to a freshly decomposed mission
```
1. GET /api/companies → companyId
2. POST /api/agents { action:"list", companyId } → agents[]
3. POST /api/missions/<id>/decompose { companyId } → subtasks (unassigned)
4. GET /api/missions/<id>/subtasks → confirm IDs + titles
5. For each subtask:
// Match by title keywords against agentName + agentRole
pick agent = agents.find(a => {
const hay = ((a.agentName || '') + ' ' + (a.agentRole || '')).toLowerCase();
return subtask.title.toLowerCase().split(/\W+/).some(w => w.length > 3 && hay.includes(w));
}) || ceoAgent;
PATCH subtasks/<sid> { companyId, assignedAgentId: agent.id }
6. POST subtasks/<root sid>/run → auto-chain handles the rest
```
### Rules
- **Prefer specialists over the CEO.** The CEO is a fine fallback but is optimized for coordination, not specialized work.
- **Don't pick the same agent for every step.** If `dependsOn` siblings (steps that could run in parallel) all share one agent, you lose parallelism — the agent processes them serially. Spread the load.
- **Tool-allowlist before reassigning.** If an agent is close-but-not-quite (missing one tool), prefer setting `attachedToolNames` on the subtask over picking a worse agent. The agent only sees the union of its `enabledTools` and the subtask's `attachedToolNames` (when set).
- **Never invent agent IDs.** If `POST /api/agents { action:"list" }` returns no agents that fit, fall back to the CEO — don't fabricate.
- **Don't use `workstationKey` to pick the agent.** It's an office-animation field, not a routing signal. See above.
---
## 2. Statuses you must respect
**Mission status**: `planning | active | paused | completed | failed | cancelled | needs_attention`
- `needs_attention` is terminal-for-automation: the mission coordinator reviewed the mission but couldn't confirm the goal was met within its pass budget, so it froze the mission for a human instead of looping. Read `coordinatorEscalatedReason` for why. The workflow will NOT auto-advance from here — a human (or you, on the user's instruction) decides what to do.
**Subtask status**:
| Status | Meaning | You can run it? |
| ------------------- | ----------------------------------------------------------- | --------------- |
| `queued` | Created, no agent assigned | No — assign first |
| `assigned` | Agent assigned, waiting | Yes |
| `in_progress` | Currently executing | No — wait |
| `done` | Completed successfully | No — already done |
| `failed` | Errored out | Yes (retry) |
| `blocked_upstream` | A depended-on step failed OR finished with tool errors; this never ran | No — fix upstream first |
**`done` ≠ clean.** A subtask can be `status: done` but carry `completedWithErrors: true` + a `lastError` of "Completed with tool errors" — it produced output but a tool inside it failed (e.g. emitted MISSING URLs, a generation 402'd). The auto-chain will NOT fire that step's `nextSubtaskIds` — it marks them `blocked_upstream` instead, so a dirty-done step never silently triggers downstream work. When inspecting a `done` step, always check `completedWithErrors` before trusting its output.
**Rule:** Never POST `/run` on a subtask whose `dependsOn` ids aren't all `done`. The server returns HTTP 409 with `pendingDeps` if you try.
---
## 3. Endpoint catalog (what to call, when)
| Goal | Endpoint |
| ------------------------------------- | -------------------------------------------------------------- |
| Create a mission | `POST /api/missions` |
| List missions | `GET /api/missions?companyId=...` |
| Read one mission | `GET /api/missions/<id>?companyId=...` |
| Update mission metadata | `PATCH /api/missions/<id>` |
| Delete mission | `DELETE /api/missions/<id>?companyId=...` |
| Auto-decompose into subtasks | `POST /api/missions/<id>/decompose` |
| Add a subtask manually | `POST /api/missions/<id>/subtasks` |
| List subtasks | `GET /api/missions/<id>/subtasks?companyId=...` |
| Update a subtask | `PATCH /api/missions/<id>/subtasks/<sid>` |
| Run a subtask | `POST /api/missions/<id>/subtasks/<sid>/run` |
| Cancel a subtask (terminal) | `POST /api/missions/<id>/subtasks/<sid>/cancel` |
| Pause a subtask (non-terminal) | `POST /api/missions/<id>/subtasks/<sid>/pause` |
| Resume a paused subtask | `POST /api/missions/<id>/subtasks/<sid>/resume` |
| Cancel a mission (terminal) | `POST /api/missions/<id>/cancel` |
| Pause a mission | `POST /api/missions/<id>/pause` |
| Resume a paused mission | `POST /api/missions/<id>/resume` |
| List skills (for `attachedSkillIds`) | `POST /api/skillManager { action:"list", companyId }` |
| List MCPs (for `attachedConnectorIds`)| `POST /api/mcpManager { action:"list", companyId }` |
| List agents (for `assignedAgentId`) | `POST /api/agents { action:"list", companyId }` |
| Share (read/clone) | `POST /api/missions/<id>/share` |
| Invite (live collab) | `POST /api/missions/<id>/invite` |
| List members + open invites | `GET /api/missions/<id>/invite?companyId=...` |
| Revoke a member | `DELETE /api/missions/<id>/invite?companyId=...&uid=<uid>` |
---
## 4. Create a mission
```http
POST /api/missions
X-TF-API-Key: <key>
Content-Type: application/json
{
"companyId": "<id>",
"title": "<short title>",
"description": "<one-paragraph problem statement>",
"priority": "low" | "medium" | "high", // optional, default medium
"tokenBudget": 50000, // optional, null = unlimited
"schedule": "0 9 * * MON", // optional cron
"scheduleLabel": "Weekly Monday 9am", // optional UI label
"scheduleEnabled": true // optional
}
```
Returns `{ id, status: 'planning', subtaskIds: [], progress: 0 }`.
**Rule:** Always write a real description — the decomposer reads it. "Do the thing" produces garbage subtasks.
---
## 5. Decompose vs. manual planning
You have two ways to build subtasks. Choose based on the user's intent:
### A. Auto-decompose (use when the user gives a high-level goal)
```http
POST /api/missions/<id>/decompose
{ "companyId": "<id>", "agentId": "<optional planner agent id>" }
```
This calls a planner agent, which writes a `planSnapshot` and creates the subtasks. The latest plan is at `mission.latestPlanSnapshotVersion`. Snapshots are versioned — you can re-decompose without losing history.
### B. Manual subtask creation (use when the user has a specific step in mind)
```http
POST /api/missions/<id>/subtasks
{
"companyId": "<id>",
"title": "<what this step does>",
"assignedAgentId": "<agent id>", // optional but required before running
"runInstructions": "<step-specific nudges>", // optional free text
"dependsOn": ["<upstream sid>", ...], // optional DAG edges
"nextSubtaskIds": ["<downstream sid>", ...], // optional DAG edges
"attachedSkillIds": [...], // optional, see §7
"attachedToolNames": [...], // optional, see §7
"attachedConnectorIds": [...] // optional, see §7
}
```
Returns the created subtask with its new `id`.
**Rule:** If the user describes a sequence ("first do X, then Y, then Z"), create the subtasks then PATCH `dependsOn` to wire them. Never expect the decomposer to know the user's ordering preference.
---
## 6. Wiring the DAG
Two arrays form the graph; keep them in sync:
```json
// In S2:
{ "dependsOn": ["S1"] }
// In S1:
{ "nextSubtaskIds": ["S2"] }
```
Common shapes:
| Shape | How to wire |
| ----------- | -------------------------------------------------------------------------- |
| Sequence | Chain `dependsOn`: A → B → C |
| Fan-out | A.nextSubtaskIds = [B, C, D]; each downstream lists A in dependsOn |
| Fan-in/diamond | D.dependsOn = [B, C]; D runs only after both finish |
**Rule:** When the user adds a new step "after" another, PATCH both the new step's `dependsOn` AND the upstream step's `nextSubtaskIds`. Forgetting one half breaks auto-chain.
---
## 7. Per-step attachments
Each subtask can override its agent's defaults *just for this step*. **Always discover the legal values before setting these fields — see §7.5 for the list endpoints.**
### Skills
```json
{ "attachedSkillIds": ["skill_video_script", "skill_brand_voice"] }
```
Skill bodies get injected into the agent's prompt as `[Attached skills]` context.
### Tools (allow-list)
```json
{ "attachedToolNames": ["Code_Assistant", "Design_Agent"] }
```
Non-empty array = the agent's tool registry is **filtered to only these tools** for this run. Empty array = no constraint, agent uses its full toolbelt. Use this to give a generalist agent a narrow focus for a specific step.
### MCP Connectors (allow-list)
```json
{ "attachedConnectorIds": ["mcp_linear", "mcp_supabase"] }
```
Same pattern: non-empty filters the connector set; empty = agent's defaults.
### Files
Mission-level files (`mission.attachments[]`) are wired via the file's `linkedSubtaskIds` array — set it to the subtask IDs that should receive that file at run time. Upload files via the dashboard UI; you typically don't create them directly via API.
**Rule:** If the user asks "give the step access to X", pick the right bucket:
- Documentation/context for the LLM → Skill
- Capability/tool to invoke → Tool
- External data source → Connector
- File payload (PDF, image, dataset) → linked attachment
---
## 7.5. Discovering legal attachment values
Before you set `attachedSkillIds`, `attachedConnectorIds`, or `attachedToolNames` on a subtask, you must list what's available. Never invent IDs or tool names — the runner silently drops unknown ones, leaving the step under-equipped.
### List skills
```http
POST /api/skillManager
X-TF-API-Key: <key>
Content-Type: application/json
{ "action": "list", "companyId": "<id>" }
```
Response:
```json
{
"success": true,
"action": "list",
"message": "Found N saved skill(s).",
"skills": [
{
"id": "summarize-url-or-file_installed_1779298872483",
"name": "Summarize URL or File",
"description": "Summarize or extract transcripts from URLs, YouTube videos, articles, PDFs and local files.",
"category": "research",
"toolsUsed": ["exec"],
"tags": ["summarize", "transcript", "youtube"],
"version": 1,
"executionCount": 0,
"successRate": 0,
"averageRating": 0,
"createdAt": "2026-05-20T17:36:41.849Z"
}
]
}
```
The `id` field is what you pass into `attachedSkillIds[]`.
### List MCP connectors
```http
POST /api/mcpManager
{ "action": "list", "companyId": "<id>" }
```
Response:
```json
{
"success": true,
"action": "list",
"connections": [
{
"id": "mcp_abc123",
"server_label": "Linear",
"server_url": "https://mcp.linear.app",
"auth_type": "oauth",
"description": "Linear issue tracker MCP",
"enabled": true,
"tools": ["create_issue", "list_issues", "update_issue"],
"toolSchemas": { "create_issue": { "description": "...", "inputSchema": {...} } }
}
],
"count": 1,
"message": "..."
}
```
Pass `connections[].id` into `attachedConnectorIds[]`. Use `{ "action": "list_platform" }` instead for platform-default MCPs available to every company without setup.
### List tool names
The canonical list of tool names is the `AGENT_TOOLS` constant in `lib/agentTools.ts` — there is no public REST endpoint that returns it. Each entry has a `function.name` field; pass those names into `attachedToolNames[]`.
Common built-in tool names you can safely reference (subject to company-level enable/disable):
| Tool name | What it does |
| ---------------------- | --------------------------------------------------------------------------------------------- |
| `Web_Browser` | Cloud headless browser (browser-use). Autonomous, set-and-forget. See §7.6. |
| `User_Browser` | Interactive E2B desktop browser with VNC stream + screenshot-to-PNG-data-URL. See §7.6. |
| `Website_Fetch` | Read/extract content from a URL (no browser). |
| `Web_Search` | Search the web for results. |
| `Researcher` | Long-running multi-source research |
| `Design_Agent` | Image-first design generation/iteration. |
| `Image_Generation` | Single-image generation (gpt-image-2 / fal models). |
| `Video_Generation` | Video synthesis (Seedance 2.0 text/image/reference-to-video). |
| `Music_Generation` | Instrumental music bed via OpenRouter Lyria. Returns a public MP3 URL. Background mode — agent polls `Background_Task_Status` with `taskType:"music"`. |
| `Voice_Generation` | Narration / voiceover via ElevenLabs TTS. Returns a public MP3 URL synchronously. Convenience over curl-from-sandbox: auto-uploads to Firebase Storage and surfaces a preview card. |
| `E2B_File_Manager` | Upload/download/list files in an E2B sandbox; can publish to Firebase Storage for public URLs. |
| `Memory_Manager` | Store/retrieve agent-scoped memory across turns. |
| `Skill_Manager` | List/install/execute skills. |
| `MCP_Manager` | List/invoke MCP connectors. |
| `Get_Credentials` | Fetch a stored credential from the vault by platform name. |
| `Manage_Credentials` | Add/update/delete vault credentials. |
| `Wait` | Sleep N seconds inside a tool loop (use between async polls). |
| `Check_Browser_Task` | Poll status of an async Web_Browser cloud task. |
| `Background_Task_Status` | Poll any background task (video/music/browser/etc) by taskId. |
| `Schedule_Task` | Create a one-shot or cron schedule. |
|
**Rule:** If you're unsure whether a tool name is valid, list the agent first (`POST /api/agents { action:"list" }`) and read its `tools[]` — those are the tool names already enabled for that agent. Setting `attachedToolNames` to a subset of `agent.tools[]` is always safe; setting it to a name the agent doesn't have works only if the company has that tool enabled globally.
### Recipe — list everything before attaching
```
1. POST /api/skillManager { action:"list", companyId } → skills[]
2. POST /api/mcpManager { action:"list", companyId } → connections[]
3. POST /api/agents { action:"list", companyId } → agents[].tools[] (effective tool names)
4. PATCH the subtask with the subset you want:
{ attachedSkillIds: [...], attachedConnectorIds: [...], attachedToolNames: [...] }
```
---
## 7.6. Picking a browser tool — `Web_Browser` vs `User_Browser`
Both tools drive a real browser, but they're used for very different jobs. Pick wrong and the step either burns tokens or returns unusable output.
### `Web_Browser` (browser-use cloud)
- **What it is:** A cloud-hosted autonomous browser agent that takes a free-form English instruction and drives the browser unattended for several minutes.
- **Async:** Yes. Returns a `taskId` in <2s; you poll with `Check_Browser_Task` (or `Background_Task_Status`) until status `finished | failed`. **You must `Wait` between polls** — back-to-back polls hammer the dyno and burn tokens.
- **Output:** A free-form text summary + an `outputFiles[]` array of files (PDFs, screenshots) the cloud agent saved. **Caveat:** the cloud agent often saves pages as PDFs, not PNGs, and `download_url` on those files is frequently `null` because browser-use's storage endpoint 404s. Outbound uploads from the cloud sandbox to public hosts (Catbox, Litterbox, Gofile, etc.) routinely fail with `Server responded with 0 code`. **Do not rely on `Web_Browser` to produce public image URLs.**
- **Use for:** Multi-step web tasks where you need an agent to make decisions inside the browser — filling forms across pages, scraping multi-pane SPAs, completing long signup flows. Use when the *outcome* is text or data, not a media artifact.
- **Required attached tools when using it:** `Web_Browser`, `Check_Browser_Task`, `Wait`, and `Memory_Manager` (to stash the taskId so a restart doesn't lose it).
### `User_Browser` (E2B desktop, VNC stream)
- **What it is:** A real Chromium running inside an E2B sandbox with a live VNC stream you can show the user. Driven action-by-action by the agent (`open_session`, `navigate`, `click`, `type`, `screenshot`, …).
- **Async:** No — each action is synchronous and returns when complete (typical action: 1–3s).
- **Output:** Each action returns structured data: `currentUrl`, `pageTitle`, `result.dataUrl` (for `screenshot`), `result.text` (for `extract`), etc. **`screenshot` returns a base64 PNG data URL inline** — the model sees the image via vision, but the data URL is *not* a public URL Seedance or any downstream service can fetch.
- **Use for:** Any case where you need (a) the user to watch what's happening, or (b) the agent to take pixel-perfect screenshots it controls, or (c) deterministic step-by-step browser interaction. Use when the *outcome* is a media artifact (screenshot, recording, downloaded file) you'll process further.
- **Required attached tools:** `User_Browser` + `E2B_File_Manager` (to write the PNG to disk and upload it to Firebase Storage to get a public URL).
### Recipe — capture N dashboard screenshots and publish as public URLs
```
1. User_Browser({ action:"open_session", url:"https://app.example.com",
instructions:"Capture brand screenshots" })
→ { sessionId, sessionUrl, sandboxId } // stash all three
2. User_Browser({ action:"type", sessionId, selector:"input[name=email]", text:"<user>" })
User_Browser({ action:"type", sessionId, selector:"input[name=password]", text:"<pass>", submit:true })
// (retrieve <user>/<pass> via Get_Credentials, never paste into runInstructions)
3. For each beat:
User_Browser({ action:"navigate", sessionId, url:"https://app.example.com/dashboard/<beat>" })
User_Browser({ action:"wait", sessionId, selector:".dashboard-ready" })
s = User_Browser({ action:"screenshot", sessionId })
// The screenshot tool writes the PNG to /tmp/user_browser_<sessionId>_<ts>.png
// inside User_Browser's E2B sandbox and returns
// { result: { dataUrl, file_path, sandboxId } }.
pub = E2B_File_Manager({ action:"upload_public", file_path: s.result.file_path })
// Do NOT pass sandboxId — every User_Browser call pins its sandbox as the
// task's persistent sandbox, so the file_manager dispatcher auto-routes
// to the same sandbox the screenshot was written into. Passing the wrong
// sandboxId is the single most common cause of "file does not exist"
// failures here.
//
// pub.publicUrl is the Firebase Storage download URL — that's what you
// forward to Seedance / Image_Generation / Video_Generation downstream.
4. User_Browser({ action:"stop", sessionId }) // release the sandbox
5. Emit { beat_<n>: pub.publicUrl } for each beat.
```
**Why this works:** `upload_public` runs inside the same sandbox as `User_Browser`, so it can read the screenshot file directly without copying bytes through the agent. It uploads to Firebase Storage and returns a long-lived public `https://` URL that any vendor (Seedance, Fal, OpenRouter) can fetch.
**Rule:** `User_Browser` + `E2B_File_Manager({ action:"upload_public" })` is the canonical path to turn a live page into a public PNG URL for downstream video / image steps. Do NOT use `Web_Browser` when the deliverable is a public screenshot URL — browser-use cloud's outbound public-host uploads are unreliable (Catbox / Litterbox / Gofile routinely return `Server responded with 0 code`).
---
## 8. Running a subtask
```http
POST /api/missions/<id>/subtasks/<sid>/run
{ "companyId": "<id>" }
```
What the server does:
1. **Dependency guard** — refuses with HTTP 409 if any `dependsOn` isn't `done`.
2. Builds prior-step context from the outputs of every `dependsOn` step (each truncated to 4000 chars). If no `dependsOn`, falls back to mission-order prior outputs.
3. Loads attached skills/tools/connectors/files.
4. Dispatches to the assigned agent via `/api/agent-task`.
5. Returns immediately; the run is **asynchronous**. You poll via `GET /api/missions/<id>` and `GET /api/missions/<id>/subtasks`.
6. On success: status → `done`, `tokenUsage` and `estimatedCostUsd` recorded, `nextSubtaskIds` auto-chained.
7. On failure: status → `failed`, downstream waiting steps walked BFS and marked `blocked_upstream`.
**Rule:** Never call `/run` more than once concurrently on the same subtask — it's locked (`lockedBy`, `lockedAt`). If you see `lastHeartbeatAt` older than 15 minutes, the lock is stale and a new run will reclaim it.
**Rule:** If you only need to run the *root* of a DAG, call `/run` on just that step. Auto-chain will fire every downstream once its deps complete.
---
## 8.5. Stopping / pausing / resuming work
Use the dedicated lifecycle endpoints — never `PATCH status:` directly, which skips the runner signal and may leave an in-flight loop chewing tokens until it hits its iteration cap.
The runner inside `/api/agent-task` checks for a cancel/pause flag at the start of every iteration (typically every 5–15s). The lifecycle endpoints below both (a) write the durable status to Firestore AND (b) signal the runner via `agentTaskResults/<taskId>` so the loop bails on its next check.
### Cancel a subtask (terminal)
```http
POST /api/missions/<id>/subtasks/<sid>/cancel
X-TF-API-Key: <key>
Content-Type: application/json
{ "companyId": "<id>", "reason": "<optional reason>" }
```
What happens:
- Runner gets `cancelRequested=true` and exits at its next iteration check (≤15s typical).
- Status → `cancelled` (terminal), `lockedBy`/`lockedAt` cleared.
- Downstream `assigned`/`queued` descendants are walked BFS and marked `blocked_upstream`.
- In-flight third-party tool calls (Web_Browser cloud task, Fal video job, etc.) keep running on the vendor side — local-only abort. Their results are discarded when they return.
Response: `{ success: true, cancelledSubtaskId, cascadedCount }`.
409 if the subtask is already terminal (`done`/`failed`/`cancelled`).
### Pause a subtask (non-terminal)
```http
POST /api/missions/<id>/subtasks/<sid>/pause
{ "companyId": "<id>", "reason": "<optional reason>" }
```
What happens:
- Runner gets `pauseRequested=true`. If mid-loop, it parks in `checkPauseOrCancel` and waits up to `PAUSE_MAX_WAIT_MS` for resume.
- Status → `paused`. Locks NOT cleared (so the parked runner can still own it).
- No cascade — downstream stays where it is.
409 if already `paused` or terminal.
### Resume a subtask
```http
POST /api/missions/<id>/subtasks/<sid>/resume
{ "companyId": "<id>" }
```
What happens:
- Clears `pauseRequested` on the runner doc; a parked loop wakes and continues from where it was.
- Status → `assigned`. If no parked runner is waiting (e.g. dyno restarted), call `POST .../run` to start a fresh run.
400 if subtask is not `paused`, or has no `assignedAgentId`.
### Mission-level
```http
POST /api/missions/<id>/cancel { "companyId": "<id>", "reason": "..." }
POST /api/missions/<id>/pause { "companyId": "<id>", "reason": "..." }
POST /api/missions/<id>/resume { "companyId": "<id>" }
```
- **Cancel mission** marks the mission `cancelled` AND fans out cancel to every non-terminal subtask. Use when the user is done with the mission entirely.
- **Pause mission** marks the mission `paused` AND pauses every `in_progress` subtask. The auto-chain refuses to dispatch new subtasks while the mission is paused.
- **Resume mission** flips mission back to `active` and any `paused` subtasks back to `assigned`. You still need to `POST .../subtasks/<sid>/run` (or `.../resume` on each paused subtask) to actually kick the work — resuming the mission alone doesn't auto-dispatch.
### Detect zombie locks
A subtask is "live" if `status === 'in_progress'` AND `lastHeartbeatAt` is within the last 15 minutes. If `status === 'in_progress'` but `lastHeartbeatAt` is older than 15 min, the dyno died mid-run; the lock is stale and a fresh `/run` will reclaim it.
```js
const isZombie =
subtask.status === 'in_progress' &&
Date.now() - new Date(subtask.lastHeartbeatAt).getTime() > 15 * 60 * 1000;
```
For a zombie, prefer `POST .../subtasks/<sid>/cancel` over a raw `/run` reclaim — cancel cleans up `runDispatching`, `currentActivity`, and any descendant fanout in one call.
**Rule:** Always prefer the lifecycle endpoints over a raw `PATCH status:`. The PATCH path skips the runner signal, so an in-flight loop keeps spending tokens until it hits the next status-aware checkpoint (which may be many minutes away if it's parked in a long tool call).
---
## 9. Scheduling
### Cron (mission-level or subtask-level)
```json
PATCH /api/missions/<id>
{
"companyId": "<id>",
"schedule": "0 9 * * MON",
"scheduleLabel": "Weekly Monday 9am",
"scheduleEnabled": true
}
```
Per-subtask cron fields: `cronExpression`, `recurrenceType`, `recurrenceInterval`, `recurrenceWeekdays`, `scheduleEnabled`. Use the subtask-level cron only when one step needs a different cadence than the mission.
### Webhook trigger (subtask-level only)
```json
PATCH /api/missions/<id>/subtasks/<sid>
{
"companyId": "<id>",
"webhookId": "wh_abc123",
"webhookEnabled": true
}
```
When a POST hits the webhook URL, the subtask runs with the webhook payload appended to its prompt.
---
## 10. Share & Invite
### Share — read + clone
```http
POST /api/missions/<id>/share
→ { "shareCode": "abc", "shareUrl": "https://app.thinkforce.ai/m/abc" }
```
The link gives anyone a preview; logged-in users can clone (which copies all subtasks, rewrites `dependsOn` / `nextSubtaskIds` to new IDs, preserves `runInstructions` / skills / files, resets run state, unassigns agents).
### Invite — live collaboration
```http
POST /api/missions/<id>/invite
→ { "inviteCode": "xyz", "inviteUrl": "https://app.thinkforce.ai/mi/xyz" }
```
Acceptee joins `mission.members[]` and gets a Y.js live session (cursor, node positions, selected step are synced realtime; durable edits still go through Firestore).
Revoke:
```http
DELETE /api/missions/<id>/invite?companyId=<id>&uid=<uid>
DELETE /api/missions/<id>/invite?companyId=<id>&all=1
```
**Rule:** Use **Share** when the user wants others to copy the mission. Use **Invite** when they want to work on it together.
---
## 11. Reading state
```http
GET /api/missions/<id>?companyId=<id>
```
Key fields you'll read:
- `status`, `progress` (0–100), `totalTokens`, `totalCostUsd`, `tokenBudget`
- `subtaskIds[]`, `agentIds[]`
- `coordinatorAgentId`, `coordinatorReviewedAt` (set when the coordinator approves the final output)
- `planSnapshots[]`, `latestPlanSnapshotVersion`
- `attachments[]` (each has `linkedSubtaskIds`)
- `members[]`, schedule fields
Subtasks:
```http
GET /api/missions/<id>/subtasks?companyId=<id>
```
Each subtask exposes:
- `status`, `currentActivity`, `lastHeartbeatAt`, `progressLog[]`
- `output`, `lastError`
- `tokenUsage`, `estimatedCostUsd`
- `dependsOn[]`, `nextSubtaskIds[]`
- `attachedSkillIds[]`, `attachedToolNames[]`, `attachedConnectorIds[]`
- `lockedBy`, `lockedAt`
**Rule:** When polling for completion, poll the **mission** (`status === 'completed'`) — not each subtask. The mission status reflects the rollup.
---
## 11.6. Platform-injected agent behavior (every subtask run)
Every time you POST `/api/missions/<id>/subtasks/<sid>/run`, the runner prepends a platform preamble onto the agent's own `agentRole` system prompt. Agents that don't know about these rules will still follow them — they're injected automatically. The rules currently in force:
| Rule | What it does |
|---|---|
| **EXECUTION** | Force multi-step completion — never stop after retrieving credentials, always continue to the action that uses them. |
| **SANDBOX NOTE** | E2B is non-root: `sudo apt-get install -y …`, `pip3 install …` (no sudo), npm/node/npx (no sudo). |
| **CREDENTIAL-FIRST DISCOVERY** | Before the first third-party API call, agents call `Manage_Credentials({ action: "list" })` to see what's stored, then `Get_Credentials({ platform: "<name>" })`. Skip only when the tool docs say "credentials auto-loaded" (Code_Assistant git ops, Clawd, Web_Browser cloud). Never paste credential values into output. |
| **PREFER E2B run_code** | The default first move for vendor-API calls, scripting, data work, and integrations is `E2B_File_Manager({ action: "run_code" })` — a few lines of curl/python/node is almost always more flexible than waiting for a typed tool. Typed tools (`Voice_Generation`, `Image_Generation`, `Music_Generation`, `Video_Generation`, `Web_Search`, …) are conveniences (auto-Firebase upload, preview surfaces) — reach for them only when you specifically want those platform conveniences. |
| **ASYNC TOOL PATTERN** | Set-and-forget: submit → store taskId in Memory_Manager → poll with the correct status tool. Never resubmit a job whose status is `processing`. |
| **TOOL ERROR HANDLING** | Classify before retrying: 401/403 → `Get_Credentials` then retry once; 400/422 → fix args then retry once; 429 → wait 10s then retry once; 504/ECONNRESET → retry once; everything else after one retry → stop and report. Never retry the same call with identical args twice. |
You don't need to repeat these in your `agentRole`. They're baked in for every mission subtask run. Use your `agentRole` for what's unique about each agent (domain expertise, voice, escalation rules), not for restating platform-wide tool discipline.
---
## 12. Coordinator agent (the orchestrator layer)
Each mission has a **coordinator agent** (`mission.coordinatorAgentId`, defaults to the company CEO). It is the orchestrator that runs at the *end* of a mission — after every subtask reaches a terminal state, the coordinator reviews whether the mission GOAL was actually met and either finalizes or closes specific gaps.
What it does on completion:
- Reads every subtask's output (flagging any `completedWithErrors` ones as unreliable).
- Writes a **mission debrief** to memory (`mission-debrief-<missionId>`).
- If the goal is met → declares `MISSION COMPLETE`, sets `coordinatorReviewedAt`.
- If not → may add a few **targeted follow-up subtasks** to close gaps, which then run and trigger one more review.
**Runaway guard (important).** The coordinator can add work that re-triggers the coordinator, so it is hard-capped by **pass count** (`mission.coordinatorPassCount`). The cap is **per-mission** and configurable (see below); the final pass is forbidden from adding work and must finalize with `MISSION COMPLETE` or `ESCALATE`. If it can't confirm the goal within the cap, the mission is frozen as **`needs_attention`** (never looped) with `coordinatorEscalatedReason` explaining why. Once `coordinatorReviewedAt` is set, the coordinator can never re-trigger. The review task itself is bounded (`maxSteps: 40`).
**Per-mission config** (set on the mission doc, e.g. via `PATCH /api/missions/<id>` or the Coordinator control on the brief):
- `coordinatorMaxPasses` — `0`–`5`, default `2`. `0` disables coordinator review entirely for that mission. Clamped to a hard ceiling of 5. Platform-wide default is overridable via the `COORDINATOR_MAX_PASSES` env var.
- `coordinatorAutoFollowups` — boolean, default `true`. When `false`, the coordinator only reviews + escalates and never adds subtasks (the conservative, zero-runaway mode).
Choose by stakes: a throwaway one-shot can run `coordinatorMaxPasses: 0` (skip review) or `1`; a high-stakes mission that should self-heal gets `3`+; set `coordinatorAutoFollowups: false` when you want the coordinator to flag gaps for a human rather than act on them.
You generally don't manipulate the coordinator unless the user asks ("use agent X to plan this mission" → set `coordinatorAgentId`). If a mission is `needs_attention`, surface `coordinatorEscalatedReason` to the user and ask how to proceed — do not blindly re-run it.
---
## 13. Error handling
| HTTP | Meaning | What to do |
| ---- | -------------------------------------------------------------------- | ------------------------------------------------------------- |
| 400 | `companyId required` or missing field | Add the missing field and retry |
| 401 | `Invalid or missing ThinkForce API key` | Stop. Tell the user their key is bad/missing |
| 404 | `Mission not found` / `Subtask not found` | Re-list to find correct id; don't guess |
| 409 | `Dependencies not satisfied` (`{ pendingDeps: [...] }`) | Either run the upstream first or remove the dep |
| 409 | `Subtask already in_progress` (lock active) | Wait + poll; don't double-dispatch |
| 5xx | Server error | Retry once with backoff; surface to user if it persists |
**Rule:** Never swallow errors silently. If a run fails, fetch `lastError` from the subtask and surface it to the user verbatim.
---
## 14. Decision rules (your operating contract)
Apply these rules before every action:
1. **Bootstrap first.** At session start, call `GET /api/companies` to resolve the user's `companyId`. Cache it. Pass it on every subsequent request. Never ask the user for it.
2. **Read before writing.** Always GET the mission + subtasks before deciding. State changes async (other agents, the user, schedulers).
3. **Don't invent IDs.** Subtask, mission, agent, and company IDs come from server responses. If you don't have one, GET the list and pick.
4. **Assign before running.** Every subtask needs `assignedAgentId` before `/run`. Decompose does NOT auto-assign — you must PATCH each subtask. Match agents by `agentName` + `agentRole` substring + `enabledTools`, never by guessing. CEO is the always-available fallback. See section 1.5.
5. **Respect the DAG.** Before calling `/run`, check `dependsOn` are all `done`. If not, either run the upstream first or tell the user why you can't proceed.
6. **Pick the right tool for "attach":**
- LLM context → Skill
- Capability → Tool
- External data → Connector
- File → linked attachment
7. **Don't double-run.** Check `status === 'in_progress'` and `lockedBy` before POST `/run`. Stale lock = `lastHeartbeatAt` older than 15 min.
8. **Wire both halves of the DAG.** When adding a dep, PATCH both the new step's `dependsOn` and the upstream's `nextSubtaskIds`.
9. **Auto-chain handles fan-out.** You only need to run the DAG's root(s). Don't run every step manually.
10. **Failures cascade.** When you see `blocked_upstream`, the fix is upstream — never re-run a blocked step directly. Fix the failed parent, then re-run the parent (auto-chain unblocks descendants).
11. **Budget guard.** If the mission has `tokenBudget` set and `totalTokens` is near the cap, warn the user before starting new runs.
12. **Surface progress and any cost the user has explicitly asked for.** When the user wants visibility into spend, report `mission.totalCostUsd` and per-step `estimatedCostUsd` directly from the GET response.
13. **Re-decompose carefully.** It creates a new plan snapshot but doesn't delete prior subtasks. If the user wants a clean re-plan, delete the old subtasks first.
14. **Confirm destructive actions.** Always ask before `DELETE` on a mission, revoking a member, or cancelling an in-flight run.
---
## 15. End-to-end recipe (copy this pattern)
User says: *"Set up a mission to launch the Tesla Roadster — concept, copy, hero image, 15s teaser. Run the design + copy in parallel after concept."*
You execute:
```
1. POST /api/missions {
companyId, title: "Launch Tesla Roadster campaign",
description: "Concept, copy, hero image, 15s teaser video",
priority: "high"
} → mission M1
2. POST /api/missions/M1/decompose { companyId }
→ returns subtasks S1 (concept), S2 (copy), S3 (hero image), S4 (teaser)
Note: all UNASSIGNED. Use subtask titles + agentName/agentRole to pick.
3. POST /api/agents { action:"list", companyId } → agents[]
→ e.g. CEO, Copywriter, Designer, Video Editor
4. GET /api/missions/M1/subtasks → confirm subtask IDs + titles
5. Assign agents to each subtask:
PATCH S1 { assignedAgentId: ceo.id } // concept → CEO (planning)
PATCH S2 { assignedAgentId: copywriter.id } // copy → Copywriter
PATCH S3 { assignedAgentId: designer.id } // hero image → Designer
PATCH S4 { assignedAgentId: videoEditor.id } // teaser → Video Editor
(Fall back to CEO for any subtask with no obvious match.)
6. Wire the DAG:
PATCH S2.dependsOn=[S1], S1.nextSubtaskIds=[S2,S3]
PATCH S3.dependsOn=[S1]
PATCH S4.dependsOn=[S2,S3], S2.nextSubtaskIds=[S4], S3.nextSubtaskIds=[S4]
(S2+S3 run in parallel after S1; S4 waits for both)
7. (Optional) Attach a Design_Agent tool to S3 + S4 to narrow scope:
PATCH S3.attachedToolNames=["Design_Agent"]
PATCH S4.attachedToolNames=["Design_Agent","Video_Generation"]
8. POST /api/missions/M1/subtasks/S1/run { companyId }
(only the root; auto-chain handles S2/S3/S4)
9. Poll GET /api/missions/M1 every ~10s until status === 'completed'
Surface progress + totalCostUsd to the user.
10. On completion, GET /api/missions/M1/subtasks, summarize S4.output
(the final teaser) and the cost roll-up.
```
---
## 16. What NOT to do
- ❌ Don't run `/run` in a loop polling — runs are async; use status polling instead.
- ❌ Don't create subtasks without an `assignedAgentId` then immediately run them (you'll get HTTP 400).
- ❌ Don't manually flip a subtask to `done` to "skip" it — the auto-chain reads outputs, and a fake `done` produces empty context for downstream steps.
- ❌ Don't share or invite without explicit user consent — both are public surfaces.
- ❌ Don't delete `planSnapshots` thinking they're cruft — they're the audit trail.
- ❌ Don't ignore `blocked_upstream` — investigate the upstream `failed` step's `lastError`.
---
If anything in this document conflicts with what you observe in the live API, **trust the API** and tell the user what you saw. This skill is a guide, not a contract.
don't have the plugin yet? install it then click "run inline in claude" again.