BLOG/BY COMPANY SIZE
THEME
BY COMPANY SIZE

OrchStep for SaaS companies

Promoting a build from staging to production sounds like one step. It's actually migrate, deploy, gate, provision — and the gaps between them are where SaaS outages live. Here's how to make the seam one workflow.

Mar 29, 2026 OrchStep Team 7 minROLE: Platform EngineerSCALE: Any

"Promote the build to prod" sounds atomic. It isn't. It's run the database migration, deploy the new version, confirm it's healthy, and — in a multi-tenant SaaS — provision or verify the tenant state that depends on the new schema. Each of those is a separate command today, run by a platform engineer who knows the order, and the gaps between them are exactly where the 2am page comes from: the migration that ran but the deploy that didn't, the deploy that shipped against a schema that hadn't migrated yet.

The fix is to make the seam a single workflow where the order is the file and a failed migration rolls itself back before the deploy ever starts.

What automation looks like for a SaaS platform team

You're building the paved road other engineers ship on. That means:

  • Promotion as one command that does migrate -> deploy -> gate in the right order, every time.
  • Migrations that roll back on failure instead of leaving the schema half-applied.
  • A production gate that does the tenant-specific work staging skips — enforced by the workflow, not by the engineer remembering.
  • The same task on a platform engineer's laptop and in the deploy pipeline, so the paved road is literally the same code path.

Where OrchStep fits

OrchStep composes tasks: a promote task can call your migrate task as a step, so the ordering and rollback live in one place and get reused everywhere. Migrations get a catch: that rolls back. Production promotion runs a tenant-provisioning branch that staging doesn't, guaranteed by an if: gate.

Here's a platform pipeline: a migrate task and a promote task that composes it.

orchstep.yml
name: platform-pipeline
defaults:
  version: "3.2.0"
  stage: staging
  tenant: shared

tasks:
  # orchstep run migrate --var stage=production
  migrate:
    steps:
      - name: plan
        func: shell
        do: echo "planning migration for {{ vars.stage }}"
        outputs:
          steps_count: "7"
      - name: apply
        func: shell
        do: echo "applying {{ steps.plan.steps_count }} migrations to {{ vars.stage }}"
        retry:
          max_attempts: 2
          interval: "2s"
          backoff_rate: 2.0
        catch:
          - name: rollback
            func: shell
            do: echo "rolling back migrations on {{ vars.stage }}"

  # orchstep run promote --var version=3.3.0 --var stage=production
  promote:
    steps:
      - name: migrate
        task: migrate
      - name: deploy
        func: shell
        do: echo "deploying {{ vars.version }} to {{ vars.stage }}"
        retry:
          max_attempts: 3
          interval: "1s"
          backoff_rate: 2.0
      - name: gate
        if: '{{ eq vars.stage "production" }}'
        then:
          - name: provision
            func: shell
            do: echo "provisioning tenant {{ vars.tenant }} on {{ vars.version }}"
          - name: verify
            func: assert
            args:
              condition: '{{ ne vars.tenant "" }}'
              message: "tenant id required for production provisioning"
        else:
          - name: smoke
            func: shell
            do: echo "smoke test {{ vars.version }} on staging"

promote calls migrate as its first step, so the migration runs — and rolls back on failure — before the deploy. The schema-then-code ordering that everyone agrees on is now enforced by the file. Production promotion runs the tenant provision branch with an assert that a tenant id is present; staging just smoke-tests. One file, one ordering, no seam to fall through.

Three workflows worth stealing

1. The composed promotion. Instead of a runbook that says "first run migrations, then deploy," the promote task calls migrate as a step. The dependency is structural — you can't deploy before migrating because the workflow runs them in order. Reuse migrate standalone too, when you only want the schema change. See Composition.

2. The self-rolling-back migration. The apply step retries transient failures, and if it still fails, the catch: rolls back so you're never left with a half-applied schema staring at a deploy that's about to ship against it. See Error Handling.

3. The tenant-aware production gate. The if: gate on the production stage provisions tenant state and asserts a tenant id is present — the production-only work staging skips, enforced rather than remembered. Promote to a named environment with --env production when staging and prod have genuinely different config. See Environments.

What you gained

ConcernRunbook + separate commandsOrchStep
Migrate-then-deploy orderengineer rememberspromote calls migrate first
Failed migrationhalf-applied schemacatch: rolls back
Transient deploy failurere-run by handretry: { max_attempts: 3 }
Prod-only tenant worka step you might skipif: gate + assert
Reusing the migrationcopy-pastecall the migrate task
Laptop vs pipelinedifferent scriptsthe same task both places

OrchStep isn't a deployment platform or a migration tool — it sequences the ones you already use (flyway, kubectl, your own CLIs) so the seams between them stop being incident sources. If a single deploy script with no real ordering or rollback covers you today, keep it. The moment promotion is "several commands in a specific order with rollback, and prod does extra," composing tasks in one workflow is what removes the 2am gap. Keep tenant credentials and connection strings out of the file with the secrets conduit.

Where to go next

Promotion is "one step" until it pages you at 2am. Make migrate -> deploy -> gate a single workflow where the order is the file.

#SAAS#PLATFORM#PROMOTION#MIGRATIONS
Try it in two minutes — one binary, no signup.
curl -fsSL https://orchstep.dev/install.sh | sh

RELATED — BY COMPANY SIZE