langclaw 0.3.0 — durable workflows¶
0.3.0 makes workflows properly durable: steps survive crashes, incomplete runs resume on restart, and fan-out failures are isolated rather than fatal.
Crash-resume¶
Each step in a @app.workflow is now a memoized, serializable unit backed by a RunStore. If the process dies mid-run — between ctx.parallel branches, mid-synthesis, anywhere — the run picks up from the last completed step on next startup.
@app.workflow("digest", input=DigestInput, max_concurrency=4)
async def digest(ctx, inp: DigestInput) -> str:
ctx.phase("fetch")
# If the process crashes here, this fan-out won't re-run on resume.
items = await ctx.parallel([
lambda c, url=url: c.tool("web_fetch", url=url)
for url in inp.sources
])
ctx.phase("summarize") # resumes here if fetch already completed
...
Opt into startup resume with:
On startup, langclaw scans the RunStore for incomplete runs and re-triggers them before the gateway starts accepting new messages.
ctx.log — inline progress¶
A lightweight log line to narrate what's happening inside a phase, without starting a new one:
ctx.phase("verify")
for claim in claims:
ctx.log(f"checking: {claim[:60]}…")
result = await ctx.tool("web_search", query=claim)
Both ctx.phase and ctx.log stream live to the originating channel as the workflow runs.
Parallel failure isolation¶
ctx.parallel now accepts return_exceptions=True — a single failing branch returns an Exception in place instead of sinking the entire fan-out:
results = await ctx.parallel(
[lambda c, name=name: c.subagent("scout", name) for name in contenders],
return_exceptions=True,
)
for name, result in zip(contenders, results):
if isinstance(result, Exception):
ctx.log(f"{name}: research failed, skipping")
continue
...
What's in 0.3¶
- Workflow durability:
RunStoreprotocol, serializable-input guard, per-step memoization ctx.log— inline log lines during a phasectx.parallelfailure isolation (return_exceptions=True)- Startup resume trigger for incomplete runs (
resume_on_startup)