Skip to content

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.

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.

src/index.ts
import { Stratal } from 'stratal'
import { AppModule } from './app.module'
export default new Stratal({ module: AppModule })

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:

src/index.ts
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' }
}
}

When you call this.runInScope(callback), Stratal:

  1. Resolves the Stratal application via the static singleton.
  2. Creates a new request-scoped DI container.
  3. Invokes your callback with the container, so you can resolve any registered service.

Cloudflare Workflows automatically handle retries and persistence for each step. Keep these patterns in mind:

  • One runInScope per 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 valuesstep.do() returns the value from your callback, which Cloudflare persists and provides to subsequent steps.
// Step results flow naturally between steps
const 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 Cloudflare
await step.do('send-notification', async () => {
return this.runInScope(async (container) => {
const notifier = container.resolve(NotificationService)
await notifier.send(user.email, 'Welcome!')
})
})

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 })
}
}