Squad Control Integration
Orchestrate AI agent tasks from Squad Control's kanban board.
Critical Rules
- - Blocked-task dedup: Before posting a blocked status update, check the thread for your most recent message. If your last message is already a blocked update AND no new messages have been posted since, do NOT post another blocked comment — exit and wait for next dispatch instead.
- Incremental thread fetching: Use
?limit=N when reading thread history to avoid loading the full thread. If you know the last message ID from a previous session, save it in agentState so the next dispatch only fetches new messages. - Use agentState for continuity: On pickup, check
task.agentState for prior session context (files modified, steps completed, decisions made). Save state mid-task via POST /api/tasks/save-state.
Quick Reference
- - Setup: INLINECODE4
- Full API: INLINECODE5
- PR template: INLINECODE6
- Review checklist: INLINECODE7
- Poll result schema: INLINECODE8
- Migration notes: INLINECODE9
- Wake listener: INLINECODE10
Required env vars: SC_API_URL, SC_API_KEY
Task Polling Flow
When a cron fires to check for tasks:
- 1. Run
~/.openclaw/skills/squad-control/scripts/poll-tasks.sh (requires SC_API_URL and SC_API_KEY env vars)
- Script is multi-workspace safe: it does
not export top-level
REPO_URL /
GITHUB_TOKEN; resolve repo/token per task from
task.workspace (or fallback top-level workspace for legacy payloads).
- Script includes overlap protection via a lock/lease guard (stale lock auto-expires).
- Script uses retry/backoff/timeouts for API calls.
- 2. If output is
HEARTBEAT_OK → no work, stop - If output contains
POLL_RESULT: → parse the JSON envelope after it:
-
pending → follow
Pickup & Dispatch below for each task
-
review.tasks → follow
Review Dispatch below for each task
-
stuck.tasks → follow
Stuck Task Recovery below for each task
⚠️ Isolated session fallback: If sessions_spawn is not available (e.g. this is an isolated cron session), do NOT hallucinate completion. Instead, hand off to the main session:
> openclaw agent --agent main --channel telegram -m "SQUAD_CONTROL_DISPATCH: pending tasks need worker sessions spawned. Use the squad-control skill. POLL_RESULT:
> <paste the full POLL_RESULT JSON here>"
>
Then reply HEARTBEAT_OK and exit. The main session has sessions_spawn and will handle dispatch.
Alternatively, call the APIs directly:
- - Pending: INLINECODE27
- Review: INLINECODE28
Parse workspace config from the response (see Multi-Workspace Response Handling below).
Wake Listener Flow
When a cron fires to run the wake listener:
- 1. Run INLINECODE29
- The script first calls
POST /api/wake/session and opens an outbound relay connection when the wake relay is configured - When a wake signal arrives, the script immediately runs
poll-tasks.sh and captures the resulting POLL_RESULT envelope - If
pending.tasks contains assigned work, the listener launches an ACP worker session directly via the local authenticated POST /api/sessions/spawn endpoint instead of routing through a second dispatcher LLM turn - If there is no direct pending work but
review.tasks or stuck.tasks remain, the listener spawns a local openclaw agent turn for the residual dispatcher work - The wake-listener cron session can then exit cleanly; future wakes will be handled by the next listener run
- If the relay is unavailable, it falls back to
GET /api/wake/poll and uses the same handoff rule after the first wake - If the local
/api/sessions/spawn endpoint returns 404/405/410/501 on an older OpenClaw build, the listener caches that capability miss for a while and falls back to the chat dispatcher path without retrying the same 404 on every wake
Use this when you want low-latency async dispatch without exposing a public OpenClaw gateway URL.
The listener stays outbound-only; the existing 15-minute poll cron remains the final fallback recovery path.
Multi-Workspace Response Handling
INLINECODE40 can return tasks from multiple workspaces when using an account-level API key. Each task includes an embedded workspace object with all config needed to work on it.
Response shape — workspace-scoped key (legacy / single-workspace):
{
"workspace": { "_id": "wsId", "name": "MyApp", "repoUrl": "...", "githubToken": "..." },
"tasks": [{ "_id": "taskId", "title": "...", "agent": { ... } }]
}
→
workspace is at the top level; tasks do not have their own workspace object.
Response shape — account-scoped key (multi-workspace):
{
"tasks": [
{
"_id": "taskId",
"title": "...",
"workspace": {
"_id": "wsId",
"name": "MyApp",
"repoUrl": "https://github.com/org/repo",
"githubToken": "ghp_...",
"agentConcurrency": 3
},
"agent": { ... }
}
]
}
→ Each task carries its own
workspace object. Tasks from different workspaces may appear in the same response.
Agent fields — always nested under task.agent (never flat on task root):
{
"agent": {
"_id": "agentId",
"name": "Cody",
"role": "Developer",
"model": "anthropic/claude-sonnet-4-6",
"soulMd": "..."
}
}
⚠️ Do NOT use task.agentName — that field does not exist. Always use task.agent.name, task.agent.model, task.agent.soulMd, task.agent._id.
Handling both shapes (backward compatible):
CODEBLOCK4
Concurrency per workspace: When tasks from multiple workspaces are returned, apply agentConcurrency per workspace independently. Do not count agents running for workspace A against workspace B's limit.
CODEBLOCK5
Stuck Task Recovery
Run two checks every cron cycle:
Check 1 — Tasks stuck in "working" with a PR deliverable:
curl -sL "${SC_API_URL}/api/tasks/list?status=working" -H "x-api-key: ${SC_API_KEY}"
For each working task where
deliverables contains a PR entry,
startedAt exists, and
startedAt is more than 30 minutes ago (and no recent activity signal) → auto-rescue by moving to review.
Before rescue, apply idempotency guards:
- 1. Skip if task already has an
autoRescuedAt marker in metadata (if available) - Skip if thread already contains an auto-rescue message for this task
- After successful rescue, write marker
autoRescuedAt=<now> (or equivalent) to prevent duplicate rescues in later cron cycles
CODEBLOCK7
Only post thread message if set-review actually changed the task state to review: "Auto-moved to review — sub-agent completed PR but didn't call set-review."
Check 2 — Tasks marked "done" with an unmerged/open PR:
curl -sL "${SC_API_URL}/api/tasks/list?status=done" -H "x-api-key: ${SC_API_KEY}"
# Filter to tasks completed in the last 2 hours that have a PR deliverable
For each recently-done task with a PR deliverable, verify the PR is actually merged:
# Extract owner/repo from workspace.repoUrl
# Extract PR number from deliverable URL (e.g. https://github.com/org/repo/pull/123 → 123)
curl -sL -H "Authorization: token ${GITHUB_TOKEN}" \
"https://api.github.com/repos/${owner}/${repo}/pulls/${PR_NUMBER}" | grep -o '"merged":[^,]*'
If
"merged":false (PR still open) → the agent skipped review. Re-open for Hawk.
Idempotency guard before creating a review task:
- 1. Search existing tasks for one already referencing this PR URL/number in INLINECODE59
- Only create a new review task if none exists
CODEBLOCK10
Post a warning to the original task thread only when a new review task is created: "⚠️ Task was marked done but PR #N is unmerged. Created review task for Hawk."
Review Dispatch
When review tasks are found, resolve the reviewer deterministically:
- 1. If
SC_REVIEWER_AGENT_ID is set, use it directly - Else query agents and pick exact role match INLINECODE61
- Else fallback to name
Hawk or role containing INLINECODE63
CODEBLOCK11
For each review task (has PR deliverable, pickedUpAt not set):
Do NOT call /api/tasks/pickup — the state machine blocks review → working transitions. Instead, spawn the reviewer directly and let them call /api/tasks/review (verdict) which transitions review → done or review → assigned.
Spawn the reviewer agent using the Review Flow template below, passing the task ID and all context directly.
Squad Lead Tasks — Merge & Complete
When the task's assigned agent is the Squad Lead (role contains "Lead" or "Orchestrator"), it means Hawk approved a PR and it's ready to merge. Do NOT just mark it done — merge the PR first.
CODEBLOCK12
If merge fails (conflicts): call /api/tasks/fail with the error — don't force-merge. Post the conflict details to the thread.
If no PR deliverable: just complete the task directly.
Concurrency Limit
Before spawning anything, check how many sub-agents are already running:
subagents(action="list")
Count agents with
status = "running". The concurrency limit comes from
workspace.agentConcurrency (default: 2 if not set).
With a single workspace: If running agents ≥ limit, skip all spawning this cycle and reply HEARTBEAT_OK.
With multiple workspaces (account-level key): Apply the limit per workspace independently. A workspace with agentConcurrency: 3 can have up to 3 agents running regardless of what other workspaces are doing. Group tasks by workspace and check each workspace's running count separately.
Also, never dispatch more than workspace.agentConcurrency tasks per workspace per cron run. The rest will be picked up on the next run.
The workspace owner can change this limit in Squad Control → Settings → Agent Concurrency.
Pickup & Dispatch
CODEBLOCK14
Post to the task thread that work is being dispatched:
CODEBLOCK15
Then spawn a sub-agent with the task's agent persona. Include the workspace name in the label so multi-workspace runs are easy to identify:
CODEBLOCK16
Spawn Prompt Template
When building this prompt, resolve workspace config as follows:
- - If
task.workspace is present (account-scoped key): use task.workspace.repoUrl, task.workspace.githubToken, INLINECODE79 - If
task.workspace is absent (workspace-scoped key): use the top-level workspace from the response
CODEBLOCK17 bash
curl -sL -X POST "${SCAPIURL}/api/tasks/set-review" \
-H "x-api-key: ${SCAPIKEY}" -H "Content-Type: application/json" \
-d '{"taskId": "${TASKID}", "agentId": "${AGENTID}", "result": "${summary}", "deliverables": [{"type": "pr", "name": "PR #N", "url": "${PR_URL}"}]}'
CODEBLOCK18
On Completion
- 1. Post findings/summary to task thread:
curl -sL -X POST "${SC_API_URL}/api/threads/send" \
-H "x-api-key: ${SC_API_KEY}" -H "Content-Type: application/json" \
-d "{\"taskId\": \"${TASK_ID}\", \"agentId\": \"${AGENT_ID}\", \"content\": \"${SUMMARY}\"}"
- 2. Create PR via GitHub API (see
references/pr-template.md) - If a reviewer agent exists → set task to review:
curl -sL -X POST "${SC_API_URL}/api/tasks/set-review" \
-H "x-api-key: ${SC_API_KEY}" -H "Content-Type: application/json" \
-d "{\"taskId\": \"${TASK_ID}\", \"agentId\": \"${AGENT_ID}\", \"result\": \"${SUMMARY}\", \"deliverables\": [{\"type\": \"pr\", \"name\": \"PR #N\", \"url\": \"${PR_URL}\"}]}"
- 4. If no reviewer → complete directly:
CODEBLOCK21
On Failure
Always report failures — don't silently mark done:
curl -sL -X POST "${SC_API_URL}/api/tasks/fail" \
-H "x-api-key: ${SC_API_KEY}" -H "Content-Type: application/json" \
-d "{\"taskId\": \"${TASK_ID}\", \"agentId\": \"${AGENT_ID}\", \"error\": \"description of what went wrong\"}"
Review Flow
When routing to a reviewer agent, spawn them with this prompt (fill in all values):
CODEBLOCK23
Note: workspace.githubToken comes from the /api/tasks/pending or /api/tasks/pickup response. Never read it from a credentials file — it may not exist on this machine.
Discovering Agents
CODEBLOCK24
Reviewer selection order:
- 1.
SC_REVIEWER_AGENT_ID (explicit override) - Exact INLINECODE87
- Fallback role contains
Reviewer or name INLINECODE89
Poll Script Tests
Run parser tests locally:
CODEBLOCK25
INLINECODE90 envelope contract is documented in:
Creating Tasks Programmatically
CODEBLOCK26
Common Mistakes
- 1. Marking done without doing work — Always post results to the thread and create a PR (if code task) before marking complete. Empty result + no thread messages = task wasn't really done.
- Sub-agent calling /complete instead of /set-review after opening a PR — This is the most common workflow violation. If a PR was opened, the ONLY valid next call is
set-review. Calling complete directly skips code review entirely and leaves an unmerged PR dangling. The stuck task recovery check now catches "done" tasks with open PRs and auto-creates a Hawk review task. - Squad Lead skipping the merge — When a task is assigned to the Squad Lead and has a PR deliverable, merge the PR to main BEFORE marking complete.
- Not passing SCAPIURL/SCAPIKEY into spawn prompt — Sub-agents can't call back to Squad Control without these. Always include them in the spawn template.
- Not using workspace.repoUrl — The pending and pickup responses include
workspace.repoUrl and workspace.githubToken. Use them — don't assume a default repo path. - Forgetting to report failure — If something goes wrong, call
/api/tasks/fail. Tasks stuck in "working" forever block the queue. - Cloning without token on private repos — Check
workspace.githubToken and use the git credential helper: git -c "credential.helper=!f() { echo username=x-access-token; echo password=<token>; }; f" clone "$REPO_URL" — never embed the token directly in the URL as it can leak via process lists, git remotes, or logs. - Not pulling latest before branching — Creates PRs against stale main, causing merge conflicts.
- Applying global concurrency instead of per-workspace — When handling tasks from multiple workspaces (account-level key), each workspace has its own
agentConcurrency limit. Don't count agents running for workspace A against workspace B's limit. - Ignoring
task.workspace and always using top-level config — Account-level keys embed workspace config directly in each task. If task.workspace is present, use it; fall back to the top-level workspace only when it's absent.
Squad Control 集成
通过 Squad Control 的看板编排 AI 智能体任务。
关键规则
- - 阻塞任务去重: 在发布阻塞状态更新前,检查线程中你最近的消息。如果你上一条消息已经是阻塞更新,且之后没有新消息发布,则不要发布另一条阻塞评论——直接退出并等待下一次调度。
- 增量线程获取: 读取线程历史时使用 ?limit=N 以避免加载完整线程。如果你知道之前会话的最后一条消息 ID,将其保存在 agentState 中,以便下次调度只获取新消息。
- 使用 agentState 保持连续性: 接取任务时,检查 task.agentState 获取之前会话的上下文(修改过的文件、完成的步骤、做出的决策)。通过 POST /api/tasks/save-state 在任务进行中保存状态。
快速参考
- - 设置: references/setup.md
- 完整 API: references/api.md
- PR 模板: references/pr-template.md
- 审查清单: references/review-checklist.md
- 轮询结果模式: references/poll-result.schema.json
- 迁移说明: references/migration-notes.md
- 唤醒监听器: scripts/wake-listener.sh
必需的环境变量:SCAPIURL、SCAPIKEY
任务轮询流程
当 cron 触发检查任务时:
- 1. 运行 ~/.openclaw/skills/squad-control/scripts/poll-tasks.sh(需要 SCAPIURL 和 SCAPIKEY 环境变量)
- 脚本支持多工作区安全:它
不导出顶层 REPO
URL / GITHUBTOKEN;根据 task.workspace 解析每个任务的仓库/令牌(对于旧版负载,回退到顶层工作区)。
- 脚本通过锁/租约保护包含重叠保护(过期锁自动过期)。
- 脚本对 API 调用使用重试/退避/超时。
- 2. 如果输出为 HEARTBEATOK → 无任务,停止
- 如果输出包含 POLLRESULT: → 解析其后的 JSON 信封:
- pending → 对每个任务执行下面的
接取和调度
- review.tasks → 对每个任务执行下面的
审查调度
- stuck.tasks → 对每个任务执行下面的
卡住任务恢复
⚠️ 隔离会话回退: 如果 sessions_spawn 不可用(例如这是隔离的 cron 会话),不要虚构完成。而是移交给主会话:
bash
openclaw agent --agent main --channel telegram -m SQUADCONTROLDISPATCH: pending tasks need worker sessions spawned. Use the squad-control skill. POLL_RESULT:
<在此粘贴完整的 POLL_RESULT JSON>
然后回复 HEARTBEATOK 并退出。主会话有 sessionsspawn 并会处理调度。
或者,直接调用 API:
- - 待处理:curl -sL ${SCAPIURL}/api/tasks/pending -H x-api-key: ${SCAPIKEY}
- 审查:curl -sL ${SCAPIURL}/api/tasks/list?status=review -H x-api-key: ${SCAPIKEY}
从响应中解析工作区配置(参见下面的多工作区响应处理)。
唤醒监听器流程
当 cron 触发运行唤醒监听器时:
- 1. 运行 ~/.openclaw/skills/squad-control/scripts/wake-listener.sh
- 脚本首先调用 POST /api/wake/session,并在配置了唤醒中继时打开出站中继连接
- 当唤醒信号到达时,脚本立即运行 poll-tasks.sh 并捕获生成的 POLL_RESULT 信封
- 如果 pending.tasks 包含分配的工作,监听器直接通过本地认证的 POST /api/sessions/spawn 端点启动 ACP 工作会话,而不是通过第二个调度器 LLM 轮次路由
- 如果没有直接待处理的工作但存在 review.tasks 或 stuck.tasks,监听器为剩余调度器工作生成一个本地 openclaw agent 轮次
- 唤醒监听器 cron 会话随后可以干净退出;未来的唤醒将由下一次监听器运行处理
- 如果中继不可用,它回退到 GET /api/wake/poll,并在第一次唤醒后使用相同的移交规则
- 如果本地 /api/sessions/spawn 端点在较旧的 OpenClaw 版本上返回 404/405/410/501,监听器会缓存该能力缺失一段时间,并回退到聊天调度器路径,而不会在每次唤醒时重试相同的 404
当你想要低延迟异步调度而不暴露公共 OpenClaw 网关 URL 时使用此功能。
监听器仅保持出站;现有的 15 分钟轮询 cron 仍然是最终的故障恢复路径。
多工作区响应处理
当使用账户级 API 密钥时,/api/tasks/pending 可以返回来自多个工作区的任务。每个任务包含一个嵌入的 workspace 对象,其中包含处理该任务所需的所有配置。
响应形状——工作区作用域密钥(旧版/单工作区):
json
{
workspace: { _id: wsId, name: MyApp, repoUrl: ..., githubToken: ... },
tasks: [{ _id: taskId, title: ..., agent: { ... } }]
}
→ workspace 在顶层;任务没有自己的 workspace 对象。
响应形状——账户作用域密钥(多工作区):
json
{
tasks: [
{
_id: taskId,
title: ...,
workspace: {
_id: wsId,
name: MyApp,
repoUrl: https://github.com/org/repo,
githubToken: ghp_...,
agentConcurrency: 3
},
agent: { ... }
}
]
}
→ 每个任务携带自己的 workspace 对象。来自不同工作区的任务可能出现在同一个响应中。
智能体字段——始终嵌套在 task.agent 下(从不平铺在任务根级别):
json
{
agent: {
_id: agentId,
name: Cody,
role: Developer,
model: anthropic/claude-sonnet-4-6,
soulMd: ...
}
}
⚠️ 不要使用 task.agentName——该字段不存在。始终使用 task.agent.name、task.agent.model、task.agent.soulMd、task.agent._id。
处理两种形状(向后兼容):
javascript
// 对于每个任务:
// - 如果 task.workspace 存在 → 直接使用
// - 如果不存在 → 使用响应中的顶层 workspace
const wsConfig = task.workspace ?? response.workspace;
const repoUrl = wsConfig.repoUrl;
const githubToken = wsConfig.githubToken;
const concurrencyLimit = wsConfig.agentConcurrency ?? 2;
每个工作区的并发: 当返回来自多个工作区的任务时,独立地对每个工作区应用 agentConcurrency。不要将对工作区 A 运行的智能体计数到工作区 B 的限制中。
javascript
// 按工作区分组,然后对每个工作区调度到并发限制
const byWorkspace = groupBy(tasks, t => (t.workspace ?? topLevelWs)._id);
for (const [wsId, wsTasks] of Object.entries(byWorkspace)) {
const ws = wsTasks[0].workspace ?? topLevelWs;
const limit = ws.agentConcurrency ?? 2;
const running = countRunningAgentsFor(wsId);
const slots = Math.max(0, limit - running);
for (const task of wsTasks.slice(0, slots)) {
dispatch(task, ws);
}
}
卡住任务恢复
每个 cron 周期运行两项检查:
检查 1——卡在working状态且有 PR 交付物的任务:
bash
curl -sL ${SCAPIURL}/api/tasks/list?status=working -H x-api-key: ${SCAPIKEY}
对于每个 working 任务,如果 deliverables 包含 PR 条目,startedAt 存在,且 startedAt 超过 30 分钟前(且没有最近的活动信号)→ 自动救援,移至审查。
在救援前,应用幂等性保护:
- 1. 如果任务元数据中已有 autoRescued