Anonymous View
Skip to content

feat(core): add FanOutWorkflow and SequenceWorkflow for sub-workflow coordination#212

Merged
jakobklippel merged 1 commit into
developfrom
feat/186-parallel-sub-workflows
Jun 15, 2026
Merged

feat(core): add FanOutWorkflow and SequenceWorkflow for sub-workflow coordination#212
jakobklippel merged 1 commit into
developfrom
feat/186-parallel-sub-workflows

Conversation

@jakobklippel

Copy link
Copy Markdown
Contributor

FanOutWorkflow runs N sub-workflows in parallel; SequenceWorkflow runs them one at a time. Both share a single aggregated callback with 'all' / 'allSettled' failure modes and accept items as an array or keyed record. Reuses the proven self-loop wait pattern from AgentWorkflow; no core changes.

What's included

@loopstack/core — new coordination primitives

  • FanOutWorkflow (name: fan_out) — parallel; on 'all'-mode failure it calls cancelChildren and the callback fires once every child has settled.
  • SequenceWorkflow (name: sequence) — cursor-based; on 'all'-mode failure remaining items are marked 'skipped'.
  • Both accept items as a keyed record (results addressable by name) or an array (results in input order with key).
  • Auto-registered via LoopCoreModule — no manual wiring needed.
  • 16 new unit tests covering happy paths, both failure modes, missing-callback edge cases, and array/record output shapes.

API alignment — items take canonical name strings only

  • { workflow: 'fetch_user' } instead of class references. Mirrors AgentWorkflow.args.tools: string[] — one way to dispatch by name across the framework.

Explicit @Workflow({ name }) on framework-shipped workflows

  • agent, chat_agent, ask_user, confirm_user, oauth, connect_github, secrets_request — matching previously auto-derived names, so no breakage for existing consumers, but the public name now lives in code instead of being inferred from class names.

Registry example

  • @loopstack/run-sub-workflow-example adds two demos (RunSubWorkflowExampleFanOutWorkflow, RunSubWorkflowExampleSequenceWorkflow) alongside the existing sequential single-child demo, reusing its sub workflow. 4 new integration tests.

Docs

  • New "Running Sub-Workflows in Parallel" and "Running Sub-Workflows in Sequence" sections in docs/build/patterns/sub-workflows.md with mode tables.
  • llms.txt regenerated.

HITL examples consolidation (rolled in to keep the working tree clean — not strictly part of #186)

  • New @loopstack/hitl-example-module consolidates the two narrower hitl-ask-user-example-workflow and hitl-confirm-example-workflow packages into a side-by-side reference covering custom-document, sub-workflow shortcut, and agent-tool patterns. Includes a decision matrix in the README.

Notes on the issue

The issue's close-out checklist references context/how-to-6-workflow-patterns.md (lines 157-158) and todo-parallel-sub-workflows.md — neither exists in the current repo; the context structure has been reworked since 2026-06-11, so nothing to update or delete there. The rest of the acceptance criteria is satisfied.

Test plan

  • npm run build from loopstack/ passes (verified locally — 59/59 packages)
  • npm test in @loopstack/core and @loopstack/run-sub-workflow-example passes (verified locally — 32 tests total)
  • Run the fan-out and sequence demos in smoke-tests Studio and verify the three children/items aggregate correctly
  • Verify the Fan Out Sub Workflows Example and Sequence Sub Workflows Example entries appear in the workflow picker

Closes #186

🤖 Generated with Claude Code

…coordination

FanOutWorkflow runs N sub-workflows in parallel; SequenceWorkflow runs them
one at a time. Both share a single aggregated callback with `'all'` /
`'allSettled'` failure modes and accept items as an array or keyed record.
Reuses the proven self-loop wait pattern from AgentWorkflow; no core changes.

- Switch FanOut/Sequence items to canonical workflow-name strings to align
  with how AgentWorkflow.args.tools already references tools.
- Add explicit `name` to framework-shipped workflows (agent, chat_agent,
  ask_user, confirm_user, oauth, connect_github, secrets_request) matching
  their auto-derived snake_case identifiers.
- New `hitl-example-module` consolidates the HITL ask/confirm examples into
  a side-by-side comparison of custom-document, sub-workflow, and agent-tool
  patterns, replacing two narrower example packages.

Closes #186
@jakobklippel jakobklippel merged commit 5893cc3 into develop Jun 15, 2026
@jakobklippel jakobklippel deleted the feat/186-parallel-sub-workflows branch June 15, 2026 10:49
jakobklippel added a commit that referenced this pull request Jun 15, 2026
* docs(getting-started): document zod v4 peer requirement (#201)

Adds a reference section explaining why Loopstack pins zod ^4 (uses
z.toJSONSchema) and that npm auto-installs it as a peer, so users
who saw zod imports in the docs don't reach for zod@^3 and trip
ERESOLVE.

Closes #191

* fix(llm-provider-module): allow bare import without forRoot (#204)

Wire the global LlmProviderRootModule from LlmProviderModule's static
@module decorator so a bare `LlmProviderModule` import registers the
provider registry, helpers, and tools with default config. `forRoot()`
and `forFeature()` are unchanged.

- Add unit tests covering bare, forRoot({}), forRoot(config), and the
  bare + forRoot(config) overlap
- Switch the package's test runner from a stale jest config to vitest +
  swc to match other feature modules
- Simplify 7 example workflow specs to use bare LlmProviderModule
- Drop the now-redundant `LlmProviderModule.forRoot({})` from
  docs/build/ai/llm-providers.md

Closes #187

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci: only run on PRs targeting main (#205)

Drop `develop` from the pull_request.branches list so CI no longer
runs on PRs into develop. workflow_dispatch is kept so the workflow
can still be triggered manually on any branch.

Closes #203

* fix(remote-client): allow bare RemoteClientModule import (#206)

Mirror the LlmProviderModule fix from #187: move the actual wiring
onto an internal `@Global() RemoteClientRootModule` and have
`RemoteClientModule`'s static `@Module` decorator import the root.
Bare `RemoteClientModule` imports now boot with `RemoteClient`,
`EnvironmentService`, `EnvironmentConfigService`, `ENVIRONMENT_CONFIG`,
and the file/exec tools registered globally. `forRoot(options)`
overrides the global config (available environments) and
`forFeature(options)` is unchanged.

Closes #202

* feat(llm-provider)!: expose text and blocks on LlmNormalizedMessage (#207)

`result.message.text` is the plain-text projection (always populated by
providers). `result.message.blocks` is the structured content. `LlmMessageDocument`
and inline `LlmMessage` args accept either field — `text` for plain content,
`blocks` for structured blocks like tool results.

Closes #190

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(common): rename MessageDocument.content to text, separate from LLM history (#208)

- MessageDocument: `content: string` → `text?: string`; tag changed from
  ['message'] to ['ui-message'] so plain UI bubbles no longer leak into LLM
  conversation history (messagesSearchTag defaults to 'message').
- LlmMessageDocument now extends MessageDocument; field shape unchanged.
- Updated frontend renderer, all registry examples, READMEs, docs, and tests.

Closes #189

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(common): auto-render sub-workflows in parent view via show option (#209)

Add `show?: 'inline' | 'link' | 'hidden'` (default `'inline'`) and `label?: string`
to `RunOptions`. The orchestrator now auto-saves a `LinkDocument` from `queue()`
based on `show`, so the parent's run view never goes blank when a sub-workflow
is launched. Studio's `LinkCard` reads live status from `useChildWorkflows`
rather than a denormalized field, and `LinkDocumentSchema.status` is removed.

All registry features, examples, and sandbox call sites drop their manual
`documentStore.save(LinkDocument, …)` pairs around `subWorkflow.run()` in favor
of `show` + `label` on the `.run()` call.

Closes #188

* fix tests

* feat(core): add FanOutWorkflow and SequenceWorkflow for sub-workflow coordination (#212)

FanOutWorkflow runs N sub-workflows in parallel; SequenceWorkflow runs them
one at a time. Both share a single aggregated callback with `'all'` /
`'allSettled'` failure modes and accept items as an array or keyed record.
Reuses the proven self-loop wait pattern from AgentWorkflow; no core changes.

- Switch FanOut/Sequence items to canonical workflow-name strings to align
  with how AgentWorkflow.args.tools already references tools.
- Add explicit `name` to framework-shipped workflows (agent, chat_agent,
  ask_user, confirm_user, oauth, connect_github, secrets_request) matching
  their auto-derived snake_case identifiers.
- New `hitl-example-module` consolidates the HITL ask/confirm examples into
  a side-by-side comparison of custom-document, sub-workflow, and agent-tool
  patterns, replacing two narrower example packages.

Closes #186

* fix: embedded sub-workflow link cards reflect live child status (#215)

- Add slim GET /workflows/:id/status endpoint and useWorkflowStatus hook
- LinkCard auto-collapses on terminal status; defers initial expanded value
  until live status arrives (no expand-then-collapse flicker on reload)
- WORKFLOW_UPDATED SSE payload carries parentId so children-list caches
  invalidate live in execution timeline, workflow list, history list
- CallbackSchema gains hasError + errorMessage; orchestrator populates them
  on dispatch so parents can branch on failure without parsing status strings
- Drop framework auto-ErrorDocument on caught exceptions; failures surface
  via workflow.errorMessage and the Retry affordance instead
- run-sub-workflow-example: add failing-sub, error-handling, show-modes
  workflows + tests (existing tests updated for new callback fields)

Closes #213

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(workflows): cluster transition types and surface guards (#216)

Move `Wait Transition` under `Transition Types` alongside Initial/Standard/Final, and add a sibling `Guarding Transitions` section right after with explicit priority and fallback rules plus a complete two-branch example. Previously both sections were standalone and buried below storage/templates content, hiding guards as a first-class branching concept. Adds a cross-link from the Wait subsection to the Human-in-the-Loop pattern doc.

Closes #197

* docs(patterns): document CallbackSchema, sub-workflow errors, and HITL patterns (#217)

- sub-workflows: add Typing the Callback Payload section (CallbackSchema field
  reference + .extend({ data }) pattern) and Error Handling section
  (branching on payload.hasError / payload.errorMessage)
- human-in-the-loop: add Choosing a HITL Pattern decision matrix, expand the
  sub-workflow shortcut section (AskUserWorkflow text/options/confirm,
  ConfirmUserWorkflow), and add Agent-Driven HITL section for the
  ask_clarification / ask_for_approval tools
- refresh frontmatter descriptions and registry references on both pages

Closes #196

* bump versions

* fix tests

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jakobklippel added a commit that referenced this pull request Jun 15, 2026
* docs(getting-started): document zod v4 peer requirement (#201)

Adds a reference section explaining why Loopstack pins zod ^4 (uses
z.toJSONSchema) and that npm auto-installs it as a peer, so users
who saw zod imports in the docs don't reach for zod@^3 and trip
ERESOLVE.

Closes #191

* fix(llm-provider-module): allow bare import without forRoot (#204)

Wire the global LlmProviderRootModule from LlmProviderModule's static
@module decorator so a bare `LlmProviderModule` import registers the
provider registry, helpers, and tools with default config. `forRoot()`
and `forFeature()` are unchanged.

- Add unit tests covering bare, forRoot({}), forRoot(config), and the
  bare + forRoot(config) overlap
- Switch the package's test runner from a stale jest config to vitest +
  swc to match other feature modules
- Simplify 7 example workflow specs to use bare LlmProviderModule
- Drop the now-redundant `LlmProviderModule.forRoot({})` from
  docs/build/ai/llm-providers.md

Closes #187

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci: only run on PRs targeting main (#205)

Drop `develop` from the pull_request.branches list so CI no longer
runs on PRs into develop. workflow_dispatch is kept so the workflow
can still be triggered manually on any branch.

Closes #203

* fix(remote-client): allow bare RemoteClientModule import (#206)

Mirror the LlmProviderModule fix from #187: move the actual wiring
onto an internal `@Global() RemoteClientRootModule` and have
`RemoteClientModule`'s static `@Module` decorator import the root.
Bare `RemoteClientModule` imports now boot with `RemoteClient`,
`EnvironmentService`, `EnvironmentConfigService`, `ENVIRONMENT_CONFIG`,
and the file/exec tools registered globally. `forRoot(options)`
overrides the global config (available environments) and
`forFeature(options)` is unchanged.

Closes #202

* feat(llm-provider)!: expose text and blocks on LlmNormalizedMessage (#207)

`result.message.text` is the plain-text projection (always populated by
providers). `result.message.blocks` is the structured content. `LlmMessageDocument`
and inline `LlmMessage` args accept either field — `text` for plain content,
`blocks` for structured blocks like tool results.

Closes #190

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(common): rename MessageDocument.content to text, separate from LLM history (#208)

- MessageDocument: `content: string` → `text?: string`; tag changed from
  ['message'] to ['ui-message'] so plain UI bubbles no longer leak into LLM
  conversation history (messagesSearchTag defaults to 'message').
- LlmMessageDocument now extends MessageDocument; field shape unchanged.
- Updated frontend renderer, all registry examples, READMEs, docs, and tests.

Closes #189

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(common): auto-render sub-workflows in parent view via show option (#209)

Add `show?: 'inline' | 'link' | 'hidden'` (default `'inline'`) and `label?: string`
to `RunOptions`. The orchestrator now auto-saves a `LinkDocument` from `queue()`
based on `show`, so the parent's run view never goes blank when a sub-workflow
is launched. Studio's `LinkCard` reads live status from `useChildWorkflows`
rather than a denormalized field, and `LinkDocumentSchema.status` is removed.

All registry features, examples, and sandbox call sites drop their manual
`documentStore.save(LinkDocument, …)` pairs around `subWorkflow.run()` in favor
of `show` + `label` on the `.run()` call.

Closes #188

* fix tests

* feat(core): add FanOutWorkflow and SequenceWorkflow for sub-workflow coordination (#212)

FanOutWorkflow runs N sub-workflows in parallel; SequenceWorkflow runs them
one at a time. Both share a single aggregated callback with `'all'` /
`'allSettled'` failure modes and accept items as an array or keyed record.
Reuses the proven self-loop wait pattern from AgentWorkflow; no core changes.

- Switch FanOut/Sequence items to canonical workflow-name strings to align
  with how AgentWorkflow.args.tools already references tools.
- Add explicit `name` to framework-shipped workflows (agent, chat_agent,
  ask_user, confirm_user, oauth, connect_github, secrets_request) matching
  their auto-derived snake_case identifiers.
- New `hitl-example-module` consolidates the HITL ask/confirm examples into
  a side-by-side comparison of custom-document, sub-workflow, and agent-tool
  patterns, replacing two narrower example packages.

Closes #186

* fix: embedded sub-workflow link cards reflect live child status (#215)

- Add slim GET /workflows/:id/status endpoint and useWorkflowStatus hook
- LinkCard auto-collapses on terminal status; defers initial expanded value
  until live status arrives (no expand-then-collapse flicker on reload)
- WORKFLOW_UPDATED SSE payload carries parentId so children-list caches
  invalidate live in execution timeline, workflow list, history list
- CallbackSchema gains hasError + errorMessage; orchestrator populates them
  on dispatch so parents can branch on failure without parsing status strings
- Drop framework auto-ErrorDocument on caught exceptions; failures surface
  via workflow.errorMessage and the Retry affordance instead
- run-sub-workflow-example: add failing-sub, error-handling, show-modes
  workflows + tests (existing tests updated for new callback fields)

Closes #213

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(workflows): cluster transition types and surface guards (#216)

Move `Wait Transition` under `Transition Types` alongside Initial/Standard/Final, and add a sibling `Guarding Transitions` section right after with explicit priority and fallback rules plus a complete two-branch example. Previously both sections were standalone and buried below storage/templates content, hiding guards as a first-class branching concept. Adds a cross-link from the Wait subsection to the Human-in-the-Loop pattern doc.

Closes #197

* docs(patterns): document CallbackSchema, sub-workflow errors, and HITL patterns (#217)

- sub-workflows: add Typing the Callback Payload section (CallbackSchema field
  reference + .extend({ data }) pattern) and Error Handling section
  (branching on payload.hasError / payload.errorMessage)
- human-in-the-loop: add Choosing a HITL Pattern decision matrix, expand the
  sub-workflow shortcut section (AskUserWorkflow text/options/confirm,
  ConfirmUserWorkflow), and add Agent-Driven HITL section for the
  ask_clarification / ask_for_approval tools
- refresh frontmatter descriptions and registry references on both pages

Closes #196

* bump versions

* fix tests

* feat(monorepo) create pg back to develop in case of failed ff

* fix changesets

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant