Skip to content

Context & State ​

The workflow context is a typed object that holds shared state across all steps. Every step in a workflow reads from and writes to the same context, making it the central place for data to flow through your pipeline.

Defining a Context Type ​

Start by defining a TypeScript interface for your workflow's data:

ts
interface TOnboardingContext {
  userId: string
  email: string
  emailVerified: boolean
  profileComplete: boolean
  welcomeEmailSent: boolean
}

Type-Safe Schemas ​

Pass your context type as a generic to @WorkflowSchema<T>(). This gives you type-checked conditions:

ts
@Workflow('onboarding')
@WorkflowSchema<TOnboardingContext>([ 
  'verify-email',
  { condition: (ctx) => ctx.emailVerified, id: 'collect-profile' }, // ctx is typed
  { condition: (ctx) => ctx.profileComplete, id: 'send-welcome' },
])
onboarding() {}

The ctx parameter in condition functions is typed as TOnboardingContext, so you get autocompletion and compile-time checks.

Providing Initial Context ​

When you start a workflow, pass the initial context object:

ts
const result = await wf.start('onboarding', { 
  userId: 'usr-42',
  email: 'alice@example.com',
  emailVerified: false,
  profileComplete: false,
  welcomeEmailSent: false,
})

This object is passed to the first step and persists across all subsequent steps.

Accessing Context in Steps ​

Option 1: Class Property (FOR_EVENT scope) ​

When the controller is event-scoped, inject context as a class property. This is the cleanest approach when multiple steps in the same controller need context access:

ts
@Injectable('FOR_EVENT') 
@Controller()
export class OnboardingController {
  @WorkflowParam('context') 
  ctx!: TOnboardingContext

  @Step('verify-email')
  verifyEmail() {
    // send verification email...
    this.ctx.emailVerified = true
  }

  @Step('collect-profile')
  collectProfile() {
    this.ctx.profileComplete = true // same ctx, different step
  }

  @Step('send-welcome')
  sendWelcome() {
    sendEmail(this.ctx.email, 'Welcome!')
    this.ctx.welcomeEmailSent = true
  }
}

Option 2: Method Parameter ​

For singleton controllers (the default scope), inject context directly into step methods:

ts
@Controller()
export class OnboardingSteps {
  @Step('verify-email')
  verifyEmail(@WorkflowParam('context') ctx: TOnboardingContext) { 
    ctx.emailVerified = true
  }

  @Step('collect-profile')
  collectProfile(@WorkflowParam('context') ctx: TOnboardingContext) {
    ctx.profileComplete = true
  }
}

INFO

Use method parameters when the controller is a singleton — class properties would be shared across concurrent workflow executions, causing data corruption. With @Injectable('FOR_EVENT'), each execution gets its own controller instance, making class properties safe.

Mutating Context ​

Context mutations in one step are visible to all subsequent steps. The context object is passed by reference:

ts
@Step('step-a')
stepA(@WorkflowParam('context') ctx: TMyContext) {
  ctx.value = 42
}

@Step('step-b')
stepB(@WorkflowParam('context') ctx: TMyContext) {
  console.log(ctx.value) // 42 — set by step-a
}

Reading Context from Output ​

After a workflow completes (or pauses), the context is available in the output:

ts
const result = await wf.start('onboarding', initialContext)

console.log(result.state.context) 
// { userId: 'usr-42', email: 'alice@example.com', emailVerified: true, ... }

The state.context reflects all mutations made by the steps that executed.

Typing MoostWf for Output ​

Type the MoostWf instance to get typed output:

ts
@Controller()
export class AppController {
  constructor(
    private wf: MoostWf<TOnboardingContext>, 
  ) {}

  async startOnboarding(userId: string, email: string) {
    const result = await this.wf.start('onboarding', {
      userId,
      email,
      emailVerified: false,
      profileComplete: false,
      welcomeEmailSent: false,
    })
    // result.state.context is typed as TOnboardingContext
  }
}

The generic parameter flows through to TFlowOutput, so result.state.context is properly typed.

Released under the MIT License.