Sub-Workflows
Sub-workflows allow you to decompose complex business logic into smaller, modular, and reusable components. In Rhythm, a workflow can invoke another workflow, creating a parent-child relationship where the parent suspends execution until the child workflow completes.
Overview
Unlike a standard Task which executes code in your host application (Python, etc.), a Sub-Workflow executes another .flow script. This is useful for:
- Modularity: Breaking down massive processes into manageable pieces.
- Reusability: Using the same orchestration logic (e.g., an "Onboarding" flow) across multiple parent workflows.
- Isolation: Each sub-workflow has its own execution ID, state, and history.
Invoking a Sub-Workflow
Sub-workflows are invoked using the Workflow.run method within a .flow script. Like tasks, these are asynchronous and should be await-ed if the parent needs to wait for the result.
// workflows/main_process.flow
// Start a sub-workflow and wait for it to finish
let userProfile = await Workflow.run("create-user-profile", {
email: Inputs.email,
username: Inputs.username
})
// Use the output of the sub-workflow
await Task.run("send-welcome-email", { profileId: userProfile.id })
API Signature
Workflow.run(workflowName: string, inputs: object): Promise<any>
workflowName: The name of the registered workflow to execute.inputs: A JSON-serializable object passed to the child workflow asInputs.- Returns: The value returned by the child workflow's
returnstatement.
Parent-Child Relationship
When a sub-workflow is started:
- The child execution is created with its
parent_workflow_idset to the parent's ID. - The parent workflow enters a
Suspendedstatus. - Once the child workflow reaches a
Completedstatus, the parent is re-enqueued, resumes exactly where it left off, and receives the child's output.
Fire-and-Forget Sub-Workflows
If you do not await the Workflow.run call, the parent will trigger the child and continue execution immediately.
[!NOTE] In "fire-and-forget" mode, the parent will not receive the output of the sub-workflow, and the child's failure will not automatically bubble up to the parent.
// Trigger a cleanup workflow without waiting for it
Workflow.run("cleanup-temp-files", { sessionId: Inputs.id })
return { status: "accepted" }
Error Handling
If a sub-workflow fails, the error is propagated to the parent. You can use standard try/catch blocks to handle these failures gracefully.
try {
await Workflow.run("provision-infrastructure", { region: "us-east-1" })
} catch (err) {
// Handle failure (e.g., notify admin or try a fallback)
await Task.run("log-incident", {
message: "Provisioning failed",
error: err
})
}
Limitations and Behavior
- Nesting Depth: While Rhythm supports deeply nested workflows, keep in mind that each level adds a layer of suspension and database tracking.
- Versioning: When a parent starts a sub-workflow, the engine looks up the latest registered version of the child workflow by name, unless a specific version is targeted.
- Atomicity: Sub-workflows are independent executions. If a parent workflow is cancelled, child workflows that were already started are not automatically cancelled by default (this behavior is subject to future updates).