Why OrchStep: compose workflows like Lego bricks
The design idea behind OrchStep: you don't carve automation from one block of bash — you snap together small, typed, well-understood pieces. Functions, steps, tasks, and modules are the bricks; your orchstep.yml is the model.
Most automation starts as one block of bash and grows by accretion. Every new requirement is another if, another ||, another exported variable bolted onto the same 300-line slab. It works right up until the day you need to change one part and discover you can't, because nothing in it is a part.
OrchStep is built on the opposite idea. You don't carve a pipeline out of one block — you snap it together from small, typed, well-understood pieces. Like Lego.
That picture is the whole mental model. Let's name the bricks.
The bricks, from smallest to largest
Functions are the typed bricks. Every step calls exactly one function, and each function does one thing well: shell runs a command, http calls an API, assert checks a condition, transform reshapes data with JavaScript, render writes a file from a template, wait polls, git and prompt do what they say. You're never writing a function — you're placing one. (The full set is here.)
Steps are placed bricks. A step is a function plus its inputs and its safety rails — retry, timeout, catch, finally. The brick knows how to fail gracefully so you don't hand-roll it each time.
Tasks are stacks of steps — an ordered column that does one job (build, deploy, migrate). And tasks call other tasks, so a ship task is just a few bricks that happen to be other stacks:
name: ship
defaults:
service: api
tasks:
build:
steps:
- { name: compile, func: shell, do: 'echo "build {{ vars.service }}"' }
test:
steps:
- { name: unit, func: shell, do: 'echo "test {{ vars.service }}"' }
# `ship` is assembled from the other two tasks, plus one deploy step.
ship:
steps:
- { name: do_build, task: build } # call a task as a brick
- { name: do_test, task: test }
- name: deploy
func: shell
do: 'echo "deploy {{ vars.service }}"'
retry: { max_attempts: 3, interval: "1s", backoff_rate: 2.0 }Modules are pre-built kits. When a stack is good enough to reuse — a canary/rollout/rollback deploy kit, a notification kit — you publish it as a module and import it into any project instead of copy-pasting it. One fix to the kit fixes every project that uses it. (Modules.)
Your orchstep.yml is the finished model — bricks, stacks, and kits assembled into the thing you actually run.
Why build this way
Composition isn't an aesthetic preference; it buys concrete properties that a slab of bash can't give you:
- You read intent, not plumbing.
task: buildandretry: { max_attempts: 3 }say what happens. Aforloop with exponential-backoff arithmetic says how, badly. - You reuse instead of copy-paste. The deploy kit lives in one place. Five services import it; none of them re-implement rollback.
- You can see the model before you build it.
orchstep run ship --dry-runresolves the whole assembly — every called task, every variable, which branch a gate takes — and prints it without executing a thing. You're inspecting the Lego instructions before snapping anything together. (Dry run.) - The same bricks run everywhere. One static binary, identical behavior locally, in any CI, and on Windows. The model you assembled on your laptop is the model that runs in production.
- The bricks delegate; they don't replace.
shellwraps thekubectl,terraform,aws, anddockeryou already run. OrchStep gives them structure — it doesn't reimplement them.
A worked intuition
Think about what changes when a requirement lands. "Add a contract-test suite before deploy."
- In the slab: find the right spot among 300 lines, hope the
set -eand the trap still behave, push a commit to test it. - In the model: add one brick —
- { name: contract, task: contract-test }— and--dry-runto see it slot in. The blast radius is one line, and you saw the result before it ran.
That difference compounds. Every brick you add is bounded; every slab edit is a gamble.
Where this isn't the answer
Composition has a floor and a ceiling. If your "automation" is genuinely three commands you run by hand, a shell alias is fine — don't assemble a model for it. And if you need a scheduler, a queue, multi-tenant RBAC, and a control plane, you need a platform; OrchStep deliberately stops short of being one. It's for the wide middle: everything that outgrew a script but doesn't justify a platform.
What you actually gain
| Concern | One block of bash | OrchStep composition |
|---|---|---|
| Add a step | edit the slab, hope | one brick, bounded |
| Reuse logic across repos | copy-paste | import a module |
| "What will this do?" | read every line | --dry-run the assembled model |
| Error handling | hand-rolled each time | retry / catch / finally on the brick |
| Runs the same in CI | "works on my machine" | one binary, identical everywhere |
| Wrap existing tools | yes, tangled | yes, structured (shell delegates) |
Where to go next
- Introduction — what OrchStep is and the principles behind it
- Your First Workflow — place your first bricks
- Composing Tasks — tasks calling tasks
- Using Modules — import reusable kits
- Previewing with Dry Run — read the instructions before you build
The point isn't that bash is bad — it's that a pile of bricks beats a carved slab the moment you need to change one. curl -fsSL https://orchstep.dev/install.sh | sh and snap a few together.
curl -fsSL https://orchstep.dev/install.sh | sh