Skip to content

Dependency resolution

You ask for one operation. The engine works out everything that has to happen first and runs it in order. This is the whole point of the tool, and it comes from two kinds of edge in a graph.

Writing {{order.order_id}} says “I need a value from the order resource”. Whichever operation extracts it becomes a prerequisite:

pay:
path: /api/v1/orders/{{order.order_id}}/pay # ← implies order.create

Most of your chain comes from this. You don’t declare it; you just use the value.

For prerequisites that produce nothing you read, but must still happen:

add_item:
path: /api/v1/cart/items
depends_on: [product.publish] # returns nothing we use
body:
product_id: "{{product.product_id}}"

Publishing a product returns no value this operation needs — but an unpublished product can’t be added to a cart. State changes like publish, activate, approve, and verify are the usual reason to reach for depends_on.

depends_on must be a sequence. As a bare scalar it is silently ignored.

The sample project’s refund.approve declares only one dependency:

resources/refunds.yaml
approve:
method: POST
path: /api/v1/admin/refunds/{{refund.refund_id}}/approve
actor: admin
depends_on: [refund.request]

Follow the edges outward — refund.request needs order.pay, which needs order.create, which needs cart.add_item, which needs product.publish, which needs product.create:

Terminal window
reqloom run refund.approve --project samples/marketplace
Terminal window
Running: refund.approve (chain of 7 steps, env=local)
[1] Running: product.create (attempt 1)
[2] Running: product.publish (attempt 1)
[3] Running: cart.add_item (attempt 1)
[4] Running: order.create (attempt 1)
[5] Running: order.pay (attempt 1)
[6] Running: refund.request (attempt 1)
[7] Running: refund.approve (attempt 1)

Seven steps from one declared dependency. Nobody wrote that list — it fell out of the graph.

Kahn’s topological sort, with a lexicographic tie-break when several operations are ready at once. That second part matters: the order is deterministic, so the same schema produces the same chain every run, and a diff in CI output means something actually changed.

Independent branches are therefore ordered predictably rather than arbitrarily — useful when you’re comparing runs, less useful as a guarantee to rely on. If two operations must happen in a specific order, say so with depends_on instead of depending on the sort.

A cycle can’t be ordered, so it’s rejected before anything runs:

Terminal window
LINT FAIL [E_CYCLE]: Circular dependency detected: order.one → order.two

reqloom lint catches this, which is why it’s worth running in a pre-commit hook. The message prints the path so you can see which edge to remove.

Within a run, an operation that’s already satisfied is skipped rather than repeated. If two branches both need product.create, it runs once:

Terminal window
OK product.create (113ms) err=—
SK product.create (0ms) err=—

SK means skipped — the value was already available.

To force an operation to execute every time it appears, mark it:

create:
force: true

Useful when each run must create fresh data rather than reuse what’s already there.

Downstream operations aren’t attempted — they’re marked BLOCK:

Terminal window
FAIL product.create (0ms) err=E_SESSION_REFRESH_FAILED
BLOCK product.publish (0ms) err=—
BLOCK cart.add_item (0ms) err=—
BLOCK order.create (0ms) err=—

BLOCK steps have no error code because they never ran. Fix the first failure — the other six are consequences, not separate problems.

Three actors appear in that seven-step chain, and their logins don’t show up in the numbering. Authentication happens inside the step that needs it, cached per actor for the session lifetime. A seven-step chain across three actors performs three logins, invisibly.

Before any request, and therefore during reqloom lint:

  1. Every referenced scope exists — an actor, resource, environment variable, or secret by that name
  2. Every depends_on target is a real operation
  3. The graph is acyclic
  4. Every operation’s chain resolves to an executable order

A seven-step chain is fine. A thirty-step chain is a smell, and usually means prerequisite data is being created that could be assumed. Two ways out:

  • Seed the data. Put long-lived fixtures in the environment ({{env.test_product_id}}) instead of creating them per run.
  • Test the endpoint, not the story. If you only care that refund.approve rejects a bad payload, an operation that references a seeded refund id needs no chain at all.

Reqloom builds the chain you declared. Declaring less is the way to a shorter one.