---
url: https://node-ts.github.io/bus/guide/workflows/state.md
description: >-
  Read and update a workflow's state from its handlers, discard changes, and
  test handlers with a fake context.
---

# State

A workflow's state tracks it as it progresses. It's stored in the configured [persistence](/persistence), and can be used to [find the workflow](/guide/workflows/handling) a message is for. This page covers reading, updating and discarding it, and testing workflow handlers.

## Defining the state

The state is a class that extends `WorkflowState`, with a `$name` that's unique among your workflow states:

```ts
import { WorkflowState } from '@node-ts/bus-core'

export class FulfilmentWorkflowState extends WorkflowState {
  // Unique among all of your workflow states
  static NAME = 'my-app/store/fulfilment-workflow-state'
  $name = FulfilmentWorkflowState.NAME

  // The workflow's own fields
  itemId: string
  customerId: string
  status: 'shipping-item' | 'emailing-receipt' | 'complete'
  shippedAt?: Date
}

```

The `$workflowId`, `$version`, `$status` and `$name` fields are managed by the bus. Values a handler returns for them are ignored.

## Reading and updating the state

The state is the second parameter of every workflow handler. It's read-only: a handler changes it by returning the fields to update, which are merged into the state and saved. Returning nothing saves no changes.

::: code-group

```ts \[Functions]
export const fulfilmentWorkflow = defineWorkflow(FulfilmentWorkflowState)
  .startedBy(ItemPurchased, ({ itemId, customerId }) => ({
    itemId,
    customerId,
    status: 'shipping-item' as const
  }))
  // The state is the second parameter of every handler
  .when(ItemShipped, (event, state) => {
    console.log('Shipped', { itemId: state.itemId, status: state.status })
    // Return the changes to save
    return { status: 'emailing-receipt' as const, shippedAt: event.shippedAt }
  })
```

```ts \[Class]
export class FulfilmentWorkflow extends Workflow<FulfilmentWorkflowState> {
  configureWorkflow(
    mapper: WorkflowMapper<FulfilmentWorkflowState, FulfilmentWorkflow>
  ): void {
    mapper
      .withState(FulfilmentWorkflowState)
      .startedBy(ItemPurchased, 'start')
      .when(ItemShipped, 'shipped')
  }

  start({ itemId, customerId }: ItemPurchased) {
    return { itemId, customerId, status: 'shipping-item' as const }
  }

  // The state is the second parameter of every handler
  shipped(event: ItemShipped, state: FulfilmentWorkflowState) {
    console.log('Shipped', { itemId: state.itemId, status: state.status })
    // Return the changes to save
    return { status: 'emailing-receipt' as const, shippedAt: event.shippedAt }
  }
}
```

:::

What a handler returns is type checked against the state: a field of the wrong type, or one that isn't in the state at any depth, doesn't compile. A handler with an annotated return type is only checked against its annotation, so leave the return type off to have its fields checked.

## Discarding state

Sometimes a handler's changes shouldn't be saved. This is particularly useful when a workflow should only start in some circumstances. Return `ctx.discard()`, or `this.discardWorkflow()` in a class workflow, to save nothing.

For example, this workflow is started by a `DocumentUploaded` event, but only for documents uploaded under `documents/`:

```ts
export const documentWorkflow = defineWorkflow(DocumentWorkflowState).startedBy(
  DocumentUploaded,
  async ({ key }, _state, ctx) => {
    if (!key.startsWith('documents/')) {
      // Ignore this upload, and don't save a new workflow
      return ctx.discard()
    }
    await ctx.send(new ReadDocument(key))
    return { key }
  }
)
```

## Testing a workflow handler

Function workflow handlers are plain functions. Get one from the workflow with `startedByHandler(Message)` or `whenHandler(Message)`, and call it with a context from `workflowContext()`. The context sends and publishes nothing, unless you pass your own functions, and its `complete` and `discard` return what the bus expects.

```ts
// In a test, with any test runner
const sent: Command[] = []
const ctx = workflowContext<DocumentWorkflowState>({
  send: async command => {
    sent.push(command)
  }
})

const result = await documentWorkflow.startedByHandler(DocumentUploaded)(
  new DocumentUploaded('documents/invoice.pdf'),
  new DocumentWorkflowState(),
  ctx
)

deepStrictEqual(result, { key: 'documents/invoice.pdf' })
deepStrictEqual(sent, [new ReadDocument('documents/invoice.pdf')])
```

## See also

* [Completing](/guide/workflows/completing) a workflow
* [`WorkflowState`](/api/bus-core/classes/WorkflowState) and [`workflowContext`](/api/bus-core/functions/workflowContext) in the API reference
