Weather
一个面向 Data & APIs 场景的 Agent 技能。原始说明:Get current weather and forecasts (no API key required).
一个面向 Data & APIs 场景的 Agent 技能。原始说明:Dispatch tasks to your ThinkForce AI agents via REST API and poll for results easily without server setup or complex configuration.
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).
companyIdYou almost never receive companyId upfront. Resolve it once at session start, cache it, and reuse on every subsequent call.
GET /api/companiesThe API key encodes which company you're operating on. Hit this endpoint first:
GET /api/companies
X-TF-API-Key: <key>
Response:
{
"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.
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.
companyId — the API key already binds you to one.GET /api/companies on every action — cache the result for the session.companyId — the key won't authenticate against a different company and you'll get 401.runInstructions, and dependency edges.dependsOn[] (upstream) and nextSubtaskIds[] (downstream). When a subtask completes, its nextSubtaskIds are auto-chained.You manage state by calling REST endpoints. Never assume state — always GET fresh data before deciding.
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.
POST /api/agents
X-TF-API-Key: <key>
Content-Type: application/json
{ "action": "list", "companyId": "<id>" }
Response:
{
"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.
| 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.
When you need to assign an agent to a subtask, match in this order:
agentName — the only short, human-meaningful label. Match by substring: "researcher", "designer", "copywriter", "marketer". This is your primary signal.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.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).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.
When you call POST /api/missions/<id>/decompose, the planner returns subtasks shaped like:
[
{ "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.
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.
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.
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.
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
dependsOn siblings (steps that could run in parallel) all share one agent, you lose parallelism — the agent processes them serially. Spread the load.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).POST /api/agents { action:"list" } returns no agents that fit, fall back to the CEO — don't fabricate.workstationKey to pick the agent. It's an office-animation field, not a routing signal. See above.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.
| 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> |
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.
You have two ways to build subtasks. Choose based on the user's intent:
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.
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.
Two arrays form the graph; keep them in sync:
// 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.
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.
{ "attachedSkillIds": ["skill_video_script", "skill_brand_voice"] }
Skill bodies get injected into the agent's prompt as [Attached skills] context.
{ "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.
{ "attachedConnectorIds": ["mcp_linear", "mcp_supabase"] }
Same pattern: non-empty filters the connector set; empty = agent's defaults.
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:
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.
POST /api/skillManager
X-TF-API-Key: <key>
Content-Type: application/json
{ "action": "list", "companyId": "<id>" }
Response:
{
"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[].
POST /api/mcpManager
{ "action": "list", "companyId": "<id>" }
Response:
{
"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.
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.
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: [...] }
Web_Browser vs User_BrowserBoth 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)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.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.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)open_session, navigate, click, type, screenshot, …).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.User_Browser + E2B_File_Manager (to write the PNG to disk and upload it to Firebase Storage to get a public URL).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).
POST /api/missions/<id>/subtasks/<sid>/run
{ "companyId": "<id>" }
What the server does:
dependsOn isn't done.dependsOn step (each truncated to 4000 chars). If no dependsOn, falls back to mission-order prior outputs./api/agent-task.GET /api/missions/<id> and GET /api/missions/<id>/subtasks.done, tokenUsage and estimatedCostUsd recorded, nextSubtaskIds auto-chained.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.
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.
POST /api/missions/<id>/subtasks/<sid>/cancel
X-TF-API-Key: <key>
Content-Type: application/json
{ "companyId": "<id>", "reason": "<optional reason>" }
What happens:
cancelRequested=true and exits at its next iteration check (≤15s typical).cancelled (terminal), lockedBy/lockedAt cleared.assigned/queued descendants are walked BFS and marked blocked_upstream.Response: { success: true, cancelledSubtaskId, cascadedCount }.
409 if the subtask is already terminal (done/failed/cancelled).
POST /api/missions/<id>/subtasks/<sid>/pause
{ "companyId": "<id>", "reason": "<optional reason>" }
What happens:
pauseRequested=true. If mid-loop, it parks in checkPauseOrCancel and waits up to PAUSE_MAX_WAIT_MS for resume.paused. Locks NOT cleared (so the parked runner can still own it).409 if already paused or terminal.
POST /api/missions/<id>/subtasks/<sid>/resume
{ "companyId": "<id>" }
What happens:
pauseRequested on the runner doc; a parked loop wakes and continues from where it was.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.
POST /api/missions/<id>/cancel { "companyId": "<id>", "reason": "..." }
POST /api/missions/<id>/pause { "companyId": "<id>", "reason": "..." }
POST /api/missions/<id>/resume { "companyId": "<id>" }
cancelled AND fans out cancel to every non-terminal subtask. Use when the user is done with the mission entirely.paused AND pauses every in_progress subtask. The auto-chain refuses to dispatch new subtasks while the mission is paused.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.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.
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).
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.
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.
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).
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:
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.
GET /api/missions/<id>?companyId=<id>
Key fields you'll read:
status, progress (0–100), totalTokens, totalCostUsd, tokenBudgetsubtaskIds[], agentIds[]coordinatorAgentId, coordinatorReviewedAt (set when the coordinator approves the final output)planSnapshots[], latestPlanSnapshotVersionattachments[] (each has linkedSubtaskIds)members[], schedule fieldsSubtasks:
GET /api/missions/<id>/subtasks?companyId=<id>
Each subtask exposes:
status, currentActivity, lastHeartbeatAt, progressLog[]output, lastErrortokenUsage, estimatedCostUsddependsOn[], nextSubtaskIds[]attachedSkillIds[], attachedToolNames[], attachedConnectorIds[]lockedBy, lockedAtRule: When polling for completion, poll the mission (status === 'completed') — not each subtask. The mission status reflects the rollup.
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" (CodeAssistant git ops, Clawd, WebBrowser 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.
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:
completedWithErrors ones as unreliable).mission-debrief-<missionId>).MISSION COMPLETE, sets coordinatorReviewedAt.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.
| 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.
Apply these rules before every action:
GET /api/companies to resolve the user's companyId. Cache it. Pass it on every subsequent request. Never ask the user for it.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./run, check dependsOn are all done. If not, either run the upstream first or tell the user why you can't proceed.status === 'in_progress' and lockedBy before POST /run. Stale lock = lastHeartbeatAt older than 15 min.dependsOn and the upstream's nextSubtaskIds.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).tokenBudget set and totalTokens is near the cap, warn the user before starting new runs.mission.totalCostUsd and per-step estimatedCostUsd directly from the GET response.DELETE on a mission, revoking a member, or cancelling an in-flight run.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.
/run in a loop polling — runs are async; use status polling instead.assignedAgentId then immediately run them (you'll get HTTP 400).done to "skip" it — the auto-chain reads outputs, and a fake done produces empty context for downstream steps.planSnapshots thinking they're cruft — they're the audit trail.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.