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.
"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.
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
| Concern | Runbook + separate commands | OrchStep |
|---|---|---|
| Migrate-then-deploy order | engineer remembers | promote calls migrate first |
| Failed migration | half-applied schema | catch: rolls back |
| Transient deploy failure | re-run by hand | retry: { max_attempts: 3 } |
| Prod-only tenant work | a step you might skip | if: gate + assert |
| Reusing the migration | copy-paste | call the migrate task |
| Laptop vs pipeline | different scripts | the 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
- Composition — call tasks from tasks, reuse the ordering
- Error Handling — retry, catch, finally for migrations
- Environments — staging and production config without script forks
- Environment Promotion — the full promote pattern
Promotion is "one step" until it pages you at 2am. Make migrate -> deploy -> gate a single workflow where the order is the file.
curl -fsSL https://orchstep.dev/install.sh | sh