🎁 Get the FREE AI Skills Starter Guide β€” Subscribe β†’
BytesAgainBytesAgain
πŸ¦€ ClawHub

Reproduce

by @sparky0520

Enables 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 whene...

Versionv1.0.4
Downloads708
TERMINAL
clawhub install recursive-spawn

πŸ“– About This Skill


name: openclaw-spawner description: > Enables 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 parallel execution, a sub-task is clearly separable from the main task, the agent detects it cannot complete the work alone within time/context limits, or the user asks to "spawn", "delegate", "parallelize", or "split" work across agents. Always use this skill β€” not ad-hoc improvisation β€” when spawning is needed. env: > Provider API key env var β€” which one depends on the model= argument passed at call time. Common ones: ANTHROPIC_API_KEY (Anthropic), OPENAI_API_KEY (OpenAI), GEMINI_API_KEY (Gemini), GROQ_API_KEY (Groq), MISTRAL_API_KEY (Mistral). Ollama requires no key. Full list: https://docs.litellm.ai/docs/providers security: > CREDENTIAL: Set the provider-specific API key env var for your chosen model. Never embed keys in payloads or snapshots. FILESYSTEM: Passing file-access tools to children grants them read/write access to arbitrary paths β€” only pass tools you trust. DATA EXPOSURE: progress_so_far is sent to the provider API and visible to the child agent β€” sanitize snapshots and strip secrets before spawning. TOOLS: Tools must be in OpenAI function-call format β€” LiteLLM translates them per provider automatically.

Openclaw Spawner

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.


Security Notes

> Credential: spawn_openclaw.py uses 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, omit > tools= β€” the child will return results in its summary text.

> Snapshot sanitization: progress_so_far is 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.


When to Spawn

Spawn a child agent when any of these are true:

  • The remaining task has a clearly separable sub-task that can run independently.
  • Two or more subtasks can proceed in parallel, saving wall-clock time.
  • The current agent's context window is approaching its limit and a fresh context would help.
  • A sub-task requires different tools, permissions, or specialization.
  • The user explicitly asks to spawn / delegate / parallelize.
  • Do not spawn for trivial one-step tasks; keep it in the current agent.


    Spawn Depth Limit

    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= when calling spawn_openclaw() from inside a child.


    Spawn Payload Schema

    Every spawn call must include these three fields:

    {
      "main_task_title": "",
      "progress_so_far": "",
      "sub_task": ""
    }
    

    Field Guidelines

    | 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.). |


    Step-by-Step Spawning Protocol

    1 β€” Decide to Spawn

    Confirm the sub-task is genuinely separable. If in doubt, handle it yourself.

    2 β€” Freeze Parent State

    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_far is 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 β€” 
    Completed:
    
  • Artifacts produced:

  • :
  • Decisions made:

  • :
  • Pending (what the child will handle):

  • 3 β€” Build the Spawn Payload

    Fill in the three required fields from the snapshot above.

    4 β€” Spawn the Openclaw Sub-Instance

    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 the summary string directly > rather than calling read_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"))
    

    5 β€” Await and Integrate Results

    1. Check is_error(summary) β€” handle failures before reading files. 2. Use read_result(path) to safely read child output; treat None as child failure. 3. Merge into parent progress snapshot. 4. Continue with the next step of the parent task β€” or spawn another child if needed.


    Spawning Strategies

    | 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 |


    Strategy A β€” Sequential

    Use spawn_openclaw(payload, depth=1) as shown in Step 4. Read result, check for errors, continue.


    Strategy B β€” Parallel Gather

    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 ...


    Strategy C β€” Fire-and-Forget

    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...")

    #### Rules for fire-and-forget

    1. Always specify result_path in sub_task β€” it is the only rendezvous. 2. Plan your merge point before spawning β€” know exactly when the parent will need the child's output. 3. Use read_result() β€” returns None safely on any filesystem error (missing file, permission denied, etc.). 4. One result file per child β€” if a child produces multiple artefacts, have it write a manifest JSON listing them all. 5. Don't fire-and-forget if parent needs the result immediately β€” use Sequential instead.


    Error Handling

    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-Patterns to Avoid

    | 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 |


    Quick Reference Card

    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?