Install
$ agentstack add skill-resonatehq-resonate-skills-resonate-recursive-fan-out-pattern-python ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Resonate Recursive Fan-Out Pattern — Python
Overview
Recursive fan-out is when a durable function spawns child invocations (either of itself or of siblings), optionally waits for them in parallel, and possibly continues recursing. Each child is its own Resonate promise; if a worker crashes, each child resumes independently.
The pattern is expressed in Python by launching multiple ctx.run(...) or ctx.rpc(...) calls before awaiting them — collect the futures first, then await them. This is different from TS's map-over-promises shape only in syntax; the semantics are identical.
When to use
- Batch processing where items are independent
- Web crawling / tree traversal with dynamic depth
- Map-reduce style workflows
- Any fan-out where each leaf is a discrete, retryable unit of work
Don't use for parallel I/O within a single step (use async clients directly inside a ctx.run envelope) or for a tight inner loop (overhead of a promise per item dominates).
Parallel fan-out in the same process
Launch children without blocking; collect futures; await them:
from __future__ import annotations
import asyncio, os, time
from typing import TYPE_CHECKING
from resonate.resonate import Resonate
if TYPE_CHECKING:
from resonate.context import Context
r = Resonate(url=os.environ.get("RESONATE_URL", "http://localhost:8001"))
async def enrich_batch(ctx: Context, order_ids: list[str]) -> list[dict]:
# Launch all children (returns futures immediately)
futures = [ctx.run(enrich_one, oid) for oid in order_ids]
# Await them all; order preserved
results = [await f for f in futures]
return results
async def enrich_one(ctx: Context, order_id: str) -> dict:
# Enrichment logic — this is a leaf
return {"order_id": order_id, "enriched": True}
Each enrich_one call is an independent durable promise. If the parent worker crashes after launching children but before awaiting, the children continue; on parent replay, awaiting the future hits the stored promise value.
Note: structured concurrency guarantees the parent cannot settle until all spawned children (even unawaited ones) complete. Explicitly awaiting them gives you the results.
Parallel fan-out across workers
Use ctx.rpc to dispatch children to remote workers:
async def parallel_enrich(ctx: Context, order_ids: list[str]) -> list[dict]:
futures = [
ctx.options(target="enrichment-workers").rpc("enrich_one", oid)
for oid in order_ids
]
return [await f for f in futures]
This horizontally scales across any enrichment-workers group. Fair queueing is the Resonate server's responsibility; your code doesn't manage worker pools.
Recursive fan-out
A durable function calling itself via rpc is legal — useful for tree traversal and crawlers:
async def crawl(ctx: Context, url: str, depth: int) -> dict:
page = await ctx.run(fetch_page, url)
if depth int:
if n int:
if n list[dict]:
results = []
for batch in _chunks(items, concurrency):
futures = [ctx.run(process_one, item) for item in batch]
results.extend([await f for f in futures])
return results
Each batch of up to concurrency runs in parallel; the next batch only starts after the previous one fully settles.
Partial failure handling
By default, if any child raises, the parent's await f re-raises. To continue on individual failures:
async def enrich_tolerant(ctx: Context, order_ids: list[str]) -> list[dict]:
futures = [ctx.run(enrich_one, oid) for oid in order_ids]
results = []
for f in futures:
try:
results.append(await f)
except Exception as err:
results.append({"error": str(err)})
return results
Each child's error is caught individually; the parent returns a mixed list of successes and error dicts.
Idempotency via stable invocation IDs
Fan-out children get deterministic ids automatically (e.g., {parent_id}.1, {parent_id}.2). The SDK assigns these based on dispatch order during replay — you do NOT pass an explicit invocation id to ctx.rpc or ctx.run; there is no id option on ctx.options in v0.7.0. Stable replay ordering is sufficient for idempotency inside a durable function.
async def enrich_batch(ctx: Context, batch_id: str, order_ids: list[str]) -> list[dict]:
# Child ids are assigned deterministically by the SDK (e.g. {parent_id}.1, .2, ...)
# Pass the function name first, then the arg
futures = [
ctx.options(version=1).rpc("enrich_one", oid)
for oid in order_ids
]
return [await f for f in futures]
If you need an explicit invocation id — for example to share a promise with an external observer — use the client-level r.rpc(id, fn, *args) from outside a durable function. Inside a durable function, ids are deterministic and managed by the runtime.
Distinct Python idioms
- Futures launched before awaited:
futures = [ctx.run(fn, x) for x in items]then[await f for f in futures]— plain list comprehensions. No special syntax needed. - Do NOT use
asyncio.gather:ctx.run(...)andctx.rpc(...)return Resonate futures, not asyncio futures.asyncio.gather(ctx.run(...), ctx.run(...))will not work correctly. The correct fan-out isfutures = [ctx.run(fn, x) for x in items]; results = [await f for f in futures]. islicefromitertoolsfor bounded parallelism — cleaner than manual indexing.try/exceptper-future — Python's narrow scoping lets you catch one child's failure without affecting siblings.- No
Promise.allorPromise.allSettled—[await f for f in futures]gives all-or-first-error semantics; per-future try/except gives allSettled semantics. - Structured concurrency — unawaited children are still joined by the runtime before the parent resolves; you don't need to track them explicitly to prevent orphans.
Related skills
resonate-basic-durable-world-usage-python—ctx.run,ctx.rpc,ctx.detached,ctx.optionsresonate-saga-pattern-python— fan-out inside a saga for parallel forward stepsresonate-human-in-the-loop-pattern-python— fan-out where each child waits for its own human approvaldurable-execution— foundational replay semantics
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: resonatehq
- Source: resonatehq/resonate-skills
- License: Apache-2.0
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.