Workflows
Stratal provides StratalWorkflow, a base class that extends Cloudflare’s native WorkflowEntrypoint with full access to your application’s DI container. This lets you build multi-step Cloudflare Workflows where each step can resolve services, use logging, and share providers with the rest of your application.
Prerequisites
Section titled “Prerequisites”Your worker entry file must export a Stratal instance as the default export. The worker base classes use the static singleton to resolve the application and its DI container at runtime.
import { Stratal } from 'stratal'import { AppModule } from './app.module'
export default new Stratal({ module: AppModule })1. Configure wrangler.jsonc
Section titled “1. Configure wrangler.jsonc”Add a workflows entry for your workflow class:
{ "workflows": [ { "name": "task-workflow", "binding": "TASK_WORKFLOW", "class_name": "TaskWorkflow" } ]}2. Export the class from your worker entry
Section titled “2. Export the class from your worker entry”Cloudflare requires Workflow classes to be named exports from the worker entry file:
import { Stratal } from 'stratal'import { AppModule } from './app.module'
export { TaskWorkflow } from './task/task-workflow'
export default new Stratal({ module: AppModule })Extend StratalWorkflow with your environment and params types, then implement the run method. Use this.runInScope() inside each step.do() to access DI services:
import type { WorkflowEvent, WorkflowStep } from 'cloudflare:workers'import { StratalWorkflow } from 'stratal/workers'import { TaskService } from './task.service'
interface TaskWorkflowParams { taskId: string}
export class TaskWorkflow extends StratalWorkflow<Env, TaskWorkflowParams> { async run( event: WorkflowEvent<TaskWorkflowParams>, step: WorkflowStep ): Promise<{ taskId: string; status: string }> { const { taskId } = event.payload
// Step 1: Validate the task exists const task = await step.do('validate-task', async () => { return this.runInScope(async (container) => { const taskService = container.resolve(TaskService) const found = taskService.findById(taskId) if (!found) { throw new TaskNotFound(taskId) } return found }) })
// Step 2: Process the task await step.do('process-task', async () => { return this.runInScope(async (container) => { const taskService = container.resolve(TaskService) taskService.updateStatus(taskId, 'processing') }) })
// Step 3: Mark as completed await step.do('complete-task', async () => { return this.runInScope(async (container) => { const taskService = container.resolve(TaskService) taskService.updateStatus(taskId, 'completed') }) })
return { taskId, status: 'completed' } }}How runInScope works
Section titled “How runInScope works”When you call this.runInScope(callback), Stratal:
- Resolves the
Stratalapplication via the static singleton. - Creates a new request-scoped DI container.
- Invokes your callback with the container, so you can resolve any registered service.
Multi-step patterns
Section titled “Multi-step patterns”Cloudflare Workflows automatically handle retries and persistence for each step. Keep these patterns in mind:
- One
runInScopeper step — each step should create its own DI scope to stay isolated. - Steps are idempotent — Cloudflare may replay steps on failure, so design your logic to be safe to re-run.
- Pass data between steps via return values —
step.do()returns the value from your callback, which Cloudflare persists and provides to subsequent steps.
// Step results flow naturally between stepsconst user = await step.do('fetch-user', async () => { return this.runInScope(async (container) => { const userService = container.resolve(UserService) return userService.findById(event.payload.userId) })})
// `user` is available here, persisted by Cloudflareawait step.do('send-notification', async () => { return this.runInScope(async (container) => { const notifier = container.resolve(NotificationService) await notifier.send(user.email, 'Welcome!') })})Triggering a workflow
Section titled “Triggering a workflow”To start a workflow from an HTTP handler, inject the Cloudflare environment via the constructor and use this.env to access the workflow binding:
import type { StratalEnv } from 'stratal'import { DI_TOKENS, inject } from 'stratal/di'import { Controller, type IController, Route, type RouterContext, uuidParamSchema } from 'stratal/router'import { z } from 'stratal/validation'import { TaskService } from './task.service'
@Controller('/api/tasks/:id/process')export class TaskProcessController implements IController { constructor( @inject(TaskService) private readonly taskService: TaskService, @inject(DI_TOKENS.CloudflareEnv) private readonly env: StratalEnv ) {}
@Route({ response: z.object({ instanceId: z.string() }), summary: 'Start the task processing workflow', params: uuidParamSchema }) async create(ctx: RouterContext) { const id = ctx.param('id')
const task = this.taskService.findById(id) if (!task) { return ctx.json({ error: 'Task not found' }, 404) }
// Start the workflow const instance = await this.env.TASK_WORKFLOW.create({ params: { taskId: id } })
return ctx.json({ instanceId: instance.id }) }}Next steps
Section titled “Next steps”- Dependency Injection to understand how the DI container and scoping works.
- Durable Objects for stateful objects with DI support.
- Service Bindings for cross-worker RPC with DI support.
- Queues for asynchronous message processing.