brainstorming
brainstorming
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or planning anything new, in software or out of it (a talk, a business, a renovation).
openclaw-spawnerEnables an Openclaw agent to spawn sub-agents (child Openclaw instances) when a task is too large, complex, or parallel to handle alone. Use this skill whenever an Openclaw agent needs to delegate work to another Openclaw agent, break a large task into concurrent sub-tasks, or hand off a portion of work mid-execution. Triggers include: task requires paralle…
Independent GitHub snapshot · Automated classification · Original documentation governs
Allows an Openclaw agent to spawn child Openclaw agents, passing them exactly the context they need to carry out their piece of work and report results back.
Helper script: scripts/spawn_openclaw.py — copy this into your project and import from it.
It contains spawn_openclaw(), spawn_openclaw_async(), is_error(), and read_result().
Multi-provider: Uses LiteLLM — pass any supported
model string via the model= argument. Default is "anthropic/claude-opus-4-6".
Requires: litellm Python package (pip install litellm) and the API key env var for
your chosen provider (e.g. ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY).
Tool format: OpenAI function-call format. LiteLLM translates to each provider's native format automatically.
Credential:
spawn_openclaw.pyuses LiteLLM to call your chosen provider. Set the matching API key env var (e.g.ANTHROPIC_API_KEY,OPENAI_API_KEY). Never put keys in a payload or snapshot.
Tool format: Tools must be in OpenAI function-call format. LiteLLM translates them to each provider's native format automatically.
Filesystem access: Passing
tools=to a child agent grants that child whatever capabilities those tools carry. File-access tools allow children to read and write arbitrary paths. Only supply tools you would trust the parent to use directly. When in doubt, omittools=— the child will return results in its summary text.
Snapshot sanitization:
progress_so_faris sent to the Anthropic API and injected into the child's context. Before spawning, review snapshots and strip any secrets, credentials, personal data, or other sensitive information.
Spawn a child agent when any of these are true:
Do not spawn for trivial one-step tasks; keep it in the current agent.
MAX_DEPTH = 3 (configurable in scripts/spawn_openclaw.py).
| Depth | Role |
|---|---|
| 0 | Root / parent agent |
| 1 | Direct child (default spawn) |
| 2 | Grandchild (only if child's sub_task explicitly permits spawning) |
| 3+ | Blocked — raises ValueError |
Always pass depth=<current_depth + 1> when calling spawn_openclaw() from inside a child.
Every spawn call must include these three fields:
{
"main_task_title": "<short human-readable title of the overall parent task>",
"progress_so_far": "<markdown summary of what has already been done, decisions made, artefacts produced, and anything the sub-agent must know to avoid redoing work>",
"sub_task": "<clear, self-contained description of exactly what this child agent must do, including expected output format and where/how to return results>"
}
| Field | Rules |
|---|---|
main_task_title | ≤ 10 words. Stable across all children of the same parent. |
progress_so_far | Include: steps completed, key decisions, files written, variables/state the child needs. Exclude: raw data the child doesn't need. Keep it dense but readable. |
sub_task | Must be self-contained. Assume the child has zero memory of the parent conversation. Include: what to do, inputs, expected output format, where to put results (file path, return value, etc.). |
Confirm the sub-task is genuinely separable. If in doubt, handle it yourself.
Before spawning, write a progress snapshot. This becomes progress_so_far for the child
and serves as the parent's own checkpoint in case it needs to resume.
Sanitize before spawning.
progress_so_faris sent to the Anthropic API and injected into the child's context. Remove any secrets, API keys, passwords, tokens, or personal data before including them in the snapshot.
## Progress Snapshot — <main_task_title>
**Completed:**
- <step 1>
- <step 2>
**Artifacts produced:**
- <file or output name>: <one-line description>
**Decisions made:**
- <decision>: <rationale>
**Pending (what the child will handle):**
- <sub-task description>
Fill in the three required fields from the snapshot above.
Import from scripts/spawn_openclaw.py:
Important: Without
tools=, the child is a pure language model — it can reason and produce text but cannot read or write files. If the sub_task requires file I/O, pass the appropriate tool definitions (e.g. Anthropic computer-use tools, custom file tools). When tools are omitted, collect the child's result from thesummarystring directly rather than callingread_result().
from spawn_openclaw import spawn_openclaw, is_error, read_result
# my_tools = [...] # OpenAI function-call format tools if the child needs file I/O
payload = {
"main_task_title": "Refactor authentication module",
"progress_so_far": (
"## Progress Snapshot\n"
"**Completed:**\n- Audited existing auth flow\n- Identified 3 outdated JWT helpers\n\n"
"**Artifacts produced:**\n- `/tmp/audit_report.md`: full list of issues\n\n"
"**Decisions made:**\n- Use PyJWT 2.x API; drop legacy HS256 fallback\n\n"
"**Pending (child handles):**\n- Rewrite auth/jwt_helpers.py per audit report"
),
"sub_task": (
"Rewrite `/src/auth/jwt_helpers.py` using PyJWT 2.x. "
"Read `/tmp/audit_report.md` for issues to fix. "
"Write the rewritten file to `/tmp/jwt_helpers_new.py` and a "
"one-paragraph summary to `/tmp/jwt_helpers_changes.md`."
),
}
try:
# Swap model= to use any LiteLLM-supported provider:
# "openai/gpt-4o", "gemini/gemini-2.0-flash", "groq/llama-3.3-70b-versatile", etc.
summary = spawn_openclaw(payload, depth=1, model="anthropic/claude-opus-4-6", tools=my_tools)
except (ValueError, FileNotFoundError) as exc:
raise # bad depth, missing payload key, or SKILL.md not found — fix the call
if is_error(summary):
print("Child failed:", summary)
else:
result = read_result("/tmp/jwt_helpers_changes.md")
if result is None:
print("WARNING: child did not write expected result file.")
If SKILL.md is not adjacent to spawn_openclaw.py, pass the path explicitly:
import pathlib
summary = spawn_openclaw(payload, depth=1, skill_path=pathlib.Path("/your/path/to/SKILL.md"))
is_error(summary) — handle failures before reading files.read_result(path) to safely read child output; treat None as child failure.| Strategy | When to use | Parent blocks? |
|---|---|---|
| Sequential | Child output is needed for next parent step | Yes, until child done |
| Parallel-gather | Multiple independent children; parent needs all before continuing | Yes, until all done |
| Fire-and-forget | Child works on a separable track; parent has its own work now | No — merge later |
Use spawn_openclaw(payload, depth=1) as shown in Step 4. Read result, check for errors, continue.
import asyncio
from spawn_openclaw import spawn_openclaw_async, is_error, read_result
async def main():
payloads = [
{
"main_task_title": "Generate market research report",
"progress_so_far": "Outline approved. Three sections assigned in parallel.",
"sub_task": "Write 'Competitive Landscape' (600 words). Save to /tmp/section_competitive.md."
},
{
"main_task_title": "Generate market research report",
"progress_so_far": "Outline approved. Three sections assigned in parallel.",
"sub_task": "Write 'Customer Segments' (600 words). Save to /tmp/section_customers.md."
},
{
"main_task_title": "Generate market research report",
"progress_so_far": "Outline approved. Three sections assigned in parallel.",
"sub_task": "Write 'Market Trends' (600 words). Save to /tmp/section_trends.md."
},
]
summaries = await asyncio.gather(
*[spawn_openclaw_async(p, depth=1, model="openai/gpt-4o", tools=my_tools) for p in payloads],
return_exceptions=True,
)
result_paths = [
"/tmp/section_competitive.md",
"/tmp/section_customers.md",
"/tmp/section_trends.md",
]
for summary, path in zip(summaries, result_paths):
if isinstance(summary, BaseException):
print(f"Child raised exception for {path}: {summary}")
continue
if is_error(summary):
print(f"Child failed for {path}:", summary)
continue
content = read_result(path)
if content is None:
print(f"WARNING: no result file at {path}")
else:
print(f"Merging {path} ({len(content)} chars)")
# merge content into parent output ...
The parent delegates a sub-task and immediately continues its own work. The child writes results to a known file path. The parent checks at a planned merge point.
Parent: ──[spawn child]──────────────────────────[merge point]──▶ continue
Child: └──[work independently]──[write result file]──▶ done
import asyncio
from spawn_openclaw import spawn_openclaw_async, is_error, read_result
async def main():
child_result_path = "/tmp/child_analysis.md"
payload = {
"main_task_title": "Refactor authentication module",
"progress_so_far": (
"Audit complete. Parent is now rewriting core auth logic. "
"Child is assigned to analyse test coverage gaps in parallel."
),
"sub_task": (
f"Read `/tmp/audit_report.md`. Identify which functions lack test coverage. "
f"Write a markdown report of gaps to `{child_result_path}`. "
f"Include function name, file, and suggested test cases for each gap."
),
}
# Spawn — returns immediately, child runs in background
child_task = asyncio.create_task(
spawn_openclaw_async(payload, depth=1, model="anthropic/claude-opus-4-6", tools=my_tools)
)
# Parent does its OWN work right now
await do_parent_work()
# Merge point — collect child
try:
summary = await child_task
except (ValueError, FileNotFoundError) as exc:
print("Child raised configuration error:", exc)
return
if is_error(summary):
print("Child failed:", summary)
else:
content = read_result(child_result_path)
if content is None:
print("WARNING: child did not write expected result file.")
else:
await integrate_child_output(content)
async def do_parent_work():
pass # replace with actual parent steps
async def integrate_child_output(text: str):
print(f"Merging {len(text)} chars from child...")
result_path in sub_task — it is the only rendezvous.read_result() — returns None safely on any filesystem error (missing file, permission denied, etc.).What raises vs. what returns an error string:
| Situation | Behaviour |
|---|---|
| Anthropic API error (network, rate limit, etc.) | Returns JSON error string — never raises |
| Empty or non-text API response | Returns JSON error string — never raises |
depth >= MAX_DEPTH | Raises ValueError — programmer error, fix your call |
Missing required payload key (main_task_title, progress_so_far, sub_task) | Raises ValueError — fix your payload |
| Non-JSON-serializable value in payload | Raises ValueError — fix your payload |
| SKILL.md not found | Raises FileNotFoundError — fix your path config |
Always check with is_error() after a successful call. Wrap the call itself in try/except if you need to handle the two programmer-error exceptions gracefully:
try:
summary = spawn_openclaw(payload, depth=1)
except (ValueError, FileNotFoundError) as exc:
print("Configuration error:", exc)
raise # or handle
if is_error(summary):
import json
err = json.loads(summary)
print("Runtime error:", err["error"])
print("Partial results at:", err.get("partial_results")) # may be None
# decide: retry, fallback, abort parent
else:
# success — read result files
result = read_result("/tmp/some_output.md")
if result is not None:
# merge result ...
pass
else:
print("WARNING: child did not write expected result file.")
| Anti-Pattern | Why It's Bad | Fix |
|---|---|---|
Sending the full conversation history as progress_so_far | Wastes tokens; child gets confused | Summarize: only what the child needs |
Including secrets in progress_so_far | Snapshot is sent to Anthropic API and visible to child | Strip API keys, passwords, tokens, and personal data before spawning |
| Passing overly-permissive tools to children | Children gain filesystem or network access beyond what their sub_task needs | Scope tools to minimum required capability; omit tools= if the task only needs text output |
Vague sub_task like "handle the rest" | Child doesn't know what to do | Be explicit: inputs, steps, output location |
| Spawning for a 2-line task | Overhead > benefit | Do it in the parent |
| Not writing a progress snapshot before spawning | Parent loses state if it crashes | Always freeze state first |
Omitting tools= when sub_task requires file I/O | Child is a pure LM — it cannot read or write files; read_result() always returns None | Pass tool definitions or collect results from the summary string instead |
Ignoring is_error() on child summary | Silent failures; parent merges nothing | Always check before reading result files |
| Fire-and-forget with no result file | No rendezvous; parent can't collect output | Always specify result_path in sub_task |
| Awaiting fire-and-forget child immediately after spawn | Defeats the purpose | Put await child_task at the merge point |
Omitting depth= when spawning from inside a child | Depth check never triggers; runaway trees | Always pass depth=current_depth + 1 |
| Child spawning further children without explicit permission | Runaway tree; hard to debug | Only spawn if sub_task explicitly says so |
SPAWN CHECKLIST
───────────────────────────────────────────
[ ] Sub-task is genuinely separable?
[ ] depth= will stay within MAX_DEPTH (default 3)?
[ ] Progress snapshot written (parent state frozen)?
[ ] main_task_title: ≤ 10 words, stable
[ ] progress_so_far: dense summary, no raw dumps
[ ] sub_task: self-contained, explicit result_path
STRATEGY SELECTION
[ ] Parent needs result before next step? → Sequential (A)
[ ] Multiple children, all needed before merge? → Parallel-gather (B)
[ ] Parent has its own work to do right now? → Fire-and-forget (C)
AFTER EVERY SPAWN
[ ] is_error(summary) checked?
[ ] read_result(path) used (handles missing files safely)?
[ ] None result handled — don't merge silently?
FIRE-AND-FORGET EXTRAS
[ ] result_path agreed before spawning?
[ ] merge point placed after parent's own work?