Skip to content

File structure

A project is a directory containing reqloom.yaml. Everything else is optional and glued on with imports:.

Fine for a handful of operations, and what the importer’s output starts as:

reqloom.yaml
version: 1
name: MyAPI
default_environment: local
environment:
baseUrl: http://localhost:3000
api_token: !secret API_TOKEN
actors:
user:
auth:
strategy: bearer
token: "{{env.api_token}}"
resources:
order:
operations:
create:
method: POST
path: /orders
actor: user
expect_status: 201
extract:
order_id: $.data.id

Past about five resources, split it. This is the layout the sample project and the importer both use:

my-api/
├── reqloom.yaml version, name, imports
├── environments/
│ ├── local.yaml
│ └── staging.yaml
├── actors/
│ ├── admin.yaml
│ ├── vendor.yaml
│ └── customer.yaml
└── resources/
├── products.yaml
├── orders.yaml
└── refunds.yaml
reqloom.yaml
version: 1
name: MarketplaceAPI
default_environment: local
imports:
- environments/*.yaml
- actors/*.yaml
- resources/*.yaml

Splitting keeps diffs readable and means two people editing different resources don’t conflict.

Three rules here are easy to get wrong, and all three fail silently.

A file’s relative path prefix decides how it’s parsed. Only three prefixes are recognised:

PrefixParsed as
actors/an actor
resources/a resource
environments/an environment

A file imported from anywhere else is read and then silently discarded. So this fails quietly:

imports:
- shared/common-actors.yaml # loaded, then thrown away

Put it in actors/, or don’t import it.

Only a trailing *.yaml or *.yml expands. Anything else is treated as a literal filename:

imports:
- resources/*.yaml # expands, sorted alphabetically
- resources/*.yml # expands
- resources/orders.yaml # a single literal file
- resources/**/*.yaml # NOT recursive — treated as a literal path
- resources/order?.yaml # NOT a pattern — literal

There is no **, no ?, no character classes. Nested subdirectories need an explicit line each:

imports:
- resources/*.yaml
- resources/admin/*.yaml

imports: also accepts a map, whose values are collapsed into one list — useful only as documentation:

imports:
environments: [environments/*.yaml]
actors: [actors/*.yaml]
resources: [resources/*.yaml]

Actor, resource, and environment files each accept two forms. The sample project uses the flat form throughout.

resources/orders.yaml — flat (name: at top level)
name: order
description: Customer orders
operations:
create:
method: POST
path: /api/v1/orders
resources/orders.yaml — wrapped (single top-level key)
order:
description: Customer orders
operations:
create:
method: POST
path: /api/v1/orders

Both give a resource with id order. With neither form, the filename stem is used — so orders.yaml would give you orders.

environments/local.yaml — wrapped
name: local
variables:
baseUrl: http://localhost:3000
admin_password: !secret ADMIN_PASSWORD
transport:
connect_timeout: 2s
environments/local.yaml — flat
baseUrl: http://localhost:3000
admin_password: !secret ADMIN_PASSWORD

In the flat form, name and transport are treated as structure, not variables. Without name:, the filename stem is the environment name — so staging.yaml becomes staging, which is what --env staging selects.

There is no environments: (plural) root key. The singular root environment: map defines exactly one environment — the one named by default_environment:

reqloom.yaml
default_environment: local
environment: # becomes the `local` environment
baseUrl: http://localhost:3000

Additional environments only exist as files under environments/. So a multi-environment project always uses imports:

imports:
- environments/*.yaml
Terminal window
reqloom run order.create --env staging

A --env name with no matching environment doesn’t error — the run proceeds with zero variables. See pitfalls.

KeyTypeDefaultNotes
versioninteger—Required. Must be 1–3
namestringUnnamed ProjectShown in output and the desktop
default_environmentstringlocalUsed when --env is absent
importssequence or map—Glob patterns
environmentmap—Variables for the default environment
actorsmap—Inline actors
resourcesmap—Inline resources
authmap—Project default for auth: { type: inherit }
transportmap—Applies to the default environment only
latency_slo{ p95_ms }—Surfaced on the desktop latency chart

Inline actors: / resources: and imported files coexist — imports are processed first, so an inline definition with the same id wins.

LimitValue
Schema file size8 MiB per file
YAML nesting depth in a body64
Hook script size1 MiB
Upload file size50 MiB

Hook scripts must be relative and inside the project root:

my-api/
├── reqloom.yaml
├── hooks/
│ └── sign-request.js
└── fixtures/
└── avatar.png
pre_request: ./hooks/sign-request.js
body_form:
avatar: "@./fixtures/avatar.png"

An absolute or escaping hook path is rejected at load with E_SCHEMA_INVALID. Upload paths are not sandboxed — see uploads.

Commit everything except secrets, which never touch the filesystem anyway:

# nothing schema-related to ignore — secrets live in the OS keychain

Keep environment files committed with non-sensitive values and !secret tags for the rest. That way a new contributor clones, adds their keychain entries, and runs.