gc sling my-agent "do this thing". That works, but real workflows have multiple steps with
dependencies between them. This tutorial shows how to define multi-step
workflows as formulas and dispatch them as a unit.
One of the main reasons agent orchestration engines like Gas City exist is to
coordinate various pieces of work without a human or shell script trying to feed
the right prompts at the right times. In Gas City, we use formulas to write
down all of the things we want to happen, and then hand them off to the agent to
do our bidding.
A formula describes the steps that need to take place, but it’s not quite step
by step instructions. As with many things in life, some things need to happen
one after another, but a lot of things can happen in parallel.
A formula is a TOML file that describes a collection of steps with dependencies,
variables, and optional control flow. To run a formula, you gc sling it to an
agent just as you would any other work.
A simple formula
Formula files use the.formula.toml extension and live in your city’s
formulas/ directory. gc init already dropped a few in there for you,
including a pancakes recipe:
needs field declares dependencies between sibling steps.
dryandwetcan run in parallelcombineneeds bothdryandwetto complete before it runscookwaits forcombineservewaits forcook
needs declarations, everything could happen at any time, which
would yield a messy kitchen, not a stack of delicious pancakes.
Inspecting formulas
Theformulas directory contains many formula files. You can ls the directory
or you can ask gc to enumerate them for you.
gc formula show compiles the formula by arranging the steps and the
dependencies, then displaying to you. In this case, the (6) count includes
the implicit root step that wraps the five recipe steps.
For the next few examples, keep using the mayor from the earlier tutorials
and add a generic worker so you have a second execution target besides the
reviewer:
claude, this city-scoped worker does not
need an agent.toml yet. Add one later if you want provider, model, or
directory overrides.
Instantiating a formula
The whole reason we write formulas is because we want to see them do things. The simplest way to see your formula do things is to sling it to an agent.mayor agent, and creates a convoy to track the grouped work. Sling handles the
full lifecycle: compile, instantiate, route, convoy, and optionally nudge the
target agent.
When you sling a formula, the result is a wisp — a lightweight, ephemeral
bead tree. Only the root bead is materialized in the store, and the steps are
read inline from the compiled recipe. Wisps are garbage-collected after they
close. This is the right choice most of the time.
For long-lived workflows where multiple agents work on different steps
independently, you want a molecule instead. A molecule materializes every
step as its own bead, each independently trackable and routable. Use gc formula cook to create a molecule, then sling individual steps wherever they need to
go:
my-project so a rig-local worker can pick it up without
crossing scope boundaries. The distinction between wisps and molecules is just
about how much state gets materialized — wisps are light and fast, molecules
give you per-step visibility and routing.
Variables
Like a function, a formula can be parameterized. You declare the parameters as variables in a[vars] section and reference them as {{name}} inside your
formula in step titles, descriptions, and other text fields.
All variables are expanded at cook or sling time — the placeholders in your
formula become concrete values in the resulting beads.
In the simplest case, a variable is just a name with a default value:
cook doesn’t echo the substituted titles. To preview the expansion, use gc formula show:
name = "world" in [vars], "world" is the default value.
Without --var name, it falls back to that default. If a variable has no
default and isn’t marked required, the placeholder stays as the literal text
{{name}} in the output — which is usually not what you want, so it’s good
practice to always provide either a default or mark it required.
Variables can also have richer definitions — descriptions, required flags,
validation:
description— human-readable explanationrequired— must be provided at instantiation timedefault— used when the caller doesn’t supply a valueenum— restrict to a set of allowed valuespattern— regex validation
--var. Here’s what the expansion looks like:
show:
cook or sling. That’s late binding, and it’s what makes formulas
reusable across different contexts.
The dependency graph
You’ve already seenneeds in the pancakes example. It gets more interesting as
formulas grow. Steps can fan out — multiple steps depending on the same
predecessor run in parallel:
test and review both wait for implement but can run in parallel with
each other. The dependency graph is a DAG — cycles are rejected at compile time.
Nested steps
When a formula gets large, you can group related steps under a parent:frontend won’t start until all of backend’s
children are done. Children are namespaced under their parent in the compiled
recipe (backend.api, backend.db), so IDs stay unique. The parent gives you a
single thing to depend on (needs = ["backend"]) instead of listing every
individual child.
You could achieve the same dependency structure with flat steps and explicit
needs — make api and db top-level, then have frontend need both.
Children are a convenience for large formulas where you’d otherwise be
maintaining long needs lists. If backend has ten sub-steps, a single needs = ["backend"] is cleaner than needs = ["api", "db", "schema", "seed", "migrate", ...]. Children also give you namespacing — two different parent
steps can each have a child called test without collision.
Control flow
It’s hopefully clear by now that the steps in a formula often execute in non-sequential, even non-deterministic order. Theneeds field is what sets up
dependencies and allows us to make order out of the chaos. The children field
allows us to wrangle that chaos across a lot of steps.
There are several other constructs that control whether a step executes at all,
and if so, how many times.
Conditions
A step can be conditionally included/excluded based on the value of a variable specified at sling or cook time.{{var}} == value or {{var}} != value. The variable is substituted first, then compared as a string. There’s no
complex expression language here — if you need more sophisticated branching, use
multiple variables and conditions across different steps.
You can see conditions take effect with gc formula show:
Loops
A step can wrap a body of sub-steps that execute multiple times:Ralph
Once a formula is cooked, conditions have been evaluated and loops have been expanded — all of that is decided up front. But sometimes you need a decision at runtime: did this step actually work? That’s what Ralph does. After the agent finishes a step, Gas City runs a check script. If the check passes, the step is done. If not, the agent tries again. The check runs after each attempt, while the formula is still executing — it’s a runtime feedback loop, not a compile-time expansion.scripts/verify.sh to check the result. If the script exits 0, the step is
done. If it exits non-zero, the agent gets another shot — up to max_attempts
times total. If all attempts fail, the step fails.
That covers the core of formulas — defining steps, wiring dependencies, parameterizing with variables, and controlling execution with conditions, loops, and Ralph.