> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stackshift.cloud/llms.txt
> Use this file to discover all available pages before exploring further.

# Durable Jobs workflows

> Use steps and events to build multi-step jobs that can retry, pause, and resume.

<Tip>
  **Live.** This area is documented as current, user-reliable behavior.
</Tip>

## Goal

Build a backend flow that survives retries and external waiting.

## Prerequisites

* A handler route exposed to StackShift

## Workflow

<Steps>
  <Step>
    Wrap side effects in named steps.
  </Step>

  <Step>
    Wait for external events with a correlation key.
  </Step>

  <Step>
    Let StackShift save progress and resume the job when the event arrives.
  </Step>

  <Step>
    Handle timeout behavior explicitly.
  </Step>
</Steps>

## Onboarding workflow

This flow creates a user, sends email, waits for verification, then unlocks the account.

<CodeGroup>
  ```ts create user -> send email -> wait -> unlock theme={null}
  stackshift.job<{ userId: string; email: string }>('userOnboarding', async ({ payload, step }) => {
    const { userId, email } = payload

    const user = await step.run('create-user', () => {
      return createUser({ id: userId, email })
    })

    await step.run('send-verification-email', () => {
      return sendVerificationEmail(user.email)
    })

    const event = await step.waitForEvent<{ userId: string }>('email.verified', {
      correlationKey: `user:${userId}`,
      timeout: '24h',
      onTimeout: 'fail',
    })

    await step.run('unlock-account', () => {
      return unlockAccount(event.payload.userId)
    })

    return { onboarded: true }
  })
  ```

  ```go Go theme={null}
  client.Job("userOnboarding", func(ctx context.Context, job stackshift.JobContext) (any, error) {
    var payload struct {
      UserID string `json:"userId"`
      Email  string `json:"email"`
    }
    _ = json.Unmarshal(job.Payload, &payload)

    _, err := job.Step.Run(ctx, "send-verification-email", func(context.Context, stackshift.HandlerInvocation) (any, error) {
      return nil, sendVerificationEmail(payload.Email)
    })
    if err != nil { return nil, err }

    event, err := job.Step.WaitForEvent(ctx, "email.verified", stackshift.WaitForEventOptions{
      CorrelationKey: "user:" + payload.UserID,
      Timeout: "24h",
      OnTimeout: "fail",
    })
    if err != nil { return nil, err }

    _, err = job.Step.Run(ctx, "unlock-account", func(context.Context, stackshift.HandlerInvocation) (any, error) {
      return nil, unlockAccount(event.Payload)
    })
    if err != nil { return nil, err }

    return map[string]any{"onboarded": true}, nil
  })
  ```
</CodeGroup>

## Resume behavior

* Before the wait, completed step results are saved.
* While waiting, the run is not consuming compute.
* When the matching event arrives, StackShift invokes the handler again with the event result.
* Completed steps return their saved outputs and the job continues after the wait.

## Failure handling

* If a step throws, the run is retried according to its retry policy.
* Completed steps are not repeated on retry.
* If the wait times out, StackShift follows the configured timeout behavior.

## Expected result

<Check>
  A multi-step job resumes from saved state instead of starting over.
</Check>

## Related guides

<CardGroup cols={2}>
  <Card title="Event Waiting + Correlation" href="/durable-jobs/event-waiting-and-correlation">
    Pause a workflow until the right external event arrives, then resume the correct run.
  </Card>

  <Card title="Retries & Failure Handling" href="/durable-jobs/retries-and-failure-handling">
    Durable Jobs retries transient failures, preserves completed steps, and gives failed work a clear recovery path.
  </Card>
</CardGroup>
