| name | write-routine |
| description | Use when building apijack YAML routines — workflow automations that chain CLI commands with variables, loops, conditions, and assertions |
Writing apijack Routines
Routines are YAML workflows that chain CLI commands. They live in ~/.<cli>/routines/ (or .apijack/routines/ in project mode).
IMPORTANT: Always Prefer Routines
ALWAYS use routines for multi-step workflows. Routines support variables, loops, randomization, and output capture — use create_routine + run_routine instead of multiple run_commands calls. Only fall back to run_commands when each call needs unique LLM-generated values that can't be expressed as variables or built-in functions.
IMPORTANT: Use Loops, Not Hardcoded Steps
When a routine repeats an operation N times, always use range or forEach — never hardcode N individual steps. Use shuffle: true for random order and reverse: true for reverse order. Use built-in functions like $_random_hex_color instead of hardcoding values.
Bad (50 hardcoded steps):
- name: create-01
command: todos create
args: { --title: "Todo 1" }
- name: create-02
command: todos create
args: { --title: "Todo 2" }
Good (1 loop):
- name: create-todos
range: [1, 50]
as: n
steps:
- name: create
command: todos create
args:
--title: "Todo $n"
Discover Commands First
Use get_routine_templates to get YAML step templates for multiple commands at once:
get_routine_templates({ commands: [
{ command: "todos create", args: { "--title": "example" } },
{ command: "todos patch", args: { "--id": "xxx", "--color": "#fff" } },
{ command: "todos delete" }
]})
This returns each command as a ready-to-use routine step with all available args shown. Use describe_command for the full argument schema (types, required/optional, descriptions).
Routine Structure
name: my-routine
description: What this routine does
variables:
project_name: "default-value"
run_id: "run-$_timestamp"
steps:
- name: step-name
command: resources create
args:
--name: "$project_name"
output: created
Built-in Variables and Functions
Variables:
$_timestamp — Unix epoch seconds
$_date — ISO date (YYYY-MM-DD)
$_uuid — random UUID
Randomization functions (evaluated fresh each time):
$_random_hex_color — random #rrggbb color
$_random_int(min,max) — random integer in range
$_random_from(a,b,c) — pick one randomly (may repeat)
$_random_distinct_from(a,b,c) — pick one randomly, no repeats until all used (then cycles)
Environment:
$_env(VAR) / $_env(VAR, default) — read from environment. .env at the project root is auto-loaded at startup; real env vars take precedence.
Array lookup:
$_find($array, field, value) — return the first element of $array where element[field] == value, or undefined if nothing matches. The array arg and the value arg resolve $refs; the field is a literal name.
$_contains($array, field, value) — returns "true" or "false". Useful in conditions.
Example — skip creation when an entity already exists:
- name: list-projects
command: projects list
output: projects
- name: create
command: projects create
args:
--name: "$name"
condition: "$_find($projects, name, $name) == undefined"
Variables can reference other variables in defaults: run_id: "run-$_timestamp".
Override at runtime: <cli> routine run my-routine --set project_name=prod.
Output Capture and References
output: alias stores a step's result. Reference fields with dot notation:
- name: create-project
command: projects create
args:
--name: "$project_name"
output: project
- name: get-project
command: projects get
args:
--id: "$project.id"
Step results are also available by step name: $create-project.id.
Use $alias.success to check if a step succeeded (boolean).
Conditions
Skip steps when a condition is false:
- name: finalize
command: resources finalize
args:
--id: "$created.id"
condition: "$created.status == ready"
Supported operators: ==, !=, or bare $ref for truthy check. The RHS may be a literal, a $ref, or the keyword undefined (for strict === undefined comparison — useful with $_find). Function calls like $_find(...) are allowed on the LHS.
Loops
forEach — iterate over arrays
- name: process-items
forEach: "$created.items"
as: item
steps:
- name: handle
command: items process
args:
--id: "$item.id"
Modifiers:
shuffle: true — randomize iteration order
reverse: true — iterate in reverse order
- name: color-randomly
forEach: "$todos"
shuffle: true
as: todo
steps:
- name: update-color
command: todos patch
args:
--id: "$todo.id"
--color: "$_random_hex_color"
- name: delete-reversed
forEach: "$todos"
reverse: true
as: todo
steps:
- name: delete
command: todos delete
args:
--id: "$todo.id"
range — iterate over numbers
- name: create-pages
range: [1, 5]
as: page
steps:
- name: create-page
command: pages create
args:
--number: "$page"
Range also supports shuffle: true and reverse: true.
Assertions
Validate step output inline:
- name: check-status
command: resources get
args:
--id: "$created.id"
assert: "$check-status.status == active"
Assertion failure stops the routine unless continueOnError: true.
Error Handling
- name: risky-step
command: might-fail
continueOnError: true
Sub-Routines
Call another routine inline:
- name: setup
command: routine run
args-positional:
- setup/environment
Meta-Commands
Built-in commands available in routines:
-
wait-until — poll until truthy result
- name: wait-ready
command: wait-until
args-positional:
- resources get
args:
--id: "$created.id"
--timeout: "60"
--interval: "5"
-
session refresh — re-authenticate mid-routine
- name: refresh
command: session refresh
Routine Commands
<cli> routine run <name>
<cli> routine run <name> --set key=value
<cli> routine run <name> --dry-run
<cli> routine validate <name>
<cli> routine test <name>
<cli> routine list
<cli> routine list --tree
Custom Resolvers
If the built-in $_* functions aren't enough, add project-specific resolvers by dropping a .ts file into .apijack/resolvers/. They show up as $_<name>(...) inside routines, just like the built-ins.
.apijack/resolvers/uppercase.ts:
import type { CustomResolverHelpers } from '@apijack/core';
export const name = '_uppercase';
export default function uppercase(argsStr?: string, helpers?: CustomResolverHelpers): string {
const raw = argsStr ?? '';
const resolved = helpers ? String(helpers.resolve(raw)) : raw;
return resolved.toUpperCase();
}
Use it in a routine:
variables:
greeting: "hello-world"
steps:
- name: create
command: resources create
args:
--title: "$_uppercase($greeting)"
Rules:
- File name (or
export const name) becomes the function name. It must start with _ to match the $_* call syntax.
- Built-in names (
_env, _find, _uuid, _random_*, etc.) are reserved — a colliding custom resolver is skipped with a stderr warning.
helpers.resolve(value) runs the arg through the full resolver (same rules as a routine value), so $refs, $_env(...), and other $_* functions inside the arg get expanded before your function sees them. Skip it to receive literal args (like $_env does).
- Resolvers are sync. Return the value directly.
Project Extensions
The .apijack/ directory at a project root is auto-loaded when the CLI runs inside that project. Drop a file in one of these subdirs and it's picked up:
| Directory | File exports | Used as |
|---|
.apijack/resolvers/*.ts | default: (argsStr?, helpers?) => unknown, optional name | Custom $_*(...) routine functions |
.apijack/commands/*.ts | default: (program, ctx) => void, optional name | Extra CLI subcommands |
.apijack/dispatchers/*.ts | default: (args, posArgs, ctx) => Promise<unknown>, optional name | Handle non-API commands invoked from routines |
.apijack/auth.ts | default: AuthStrategy, optional onChallenge | Project-level auth strategy |
.apijack/routines/*.yaml | Routine YAML | Available via routine run <name> |
.apijack/settings.json | { customCommands: { defaults: { requiresAuth } } } | Framework defaults for extensions |
Opting custom commands into auth
Custom commands and dispatchers get ctx.session = null by default (only generated OpenAPI commands auto-resolve). To get a non-null session, add export const requiresAuth = true alongside the registrar:
import type { CommandRegistrar } from "@apijack/core";
export const name = "sync";
export const requiresAuth = true;
const register: CommandRegistrar<true> = (program, ctx) => {
program.command("sync").action(async () => {
});
};
export default register;
Dispatchers: same export, DispatcherHandler<true> for the typed form.
Flip the default for every extension in .apijack/settings.json:
{ "customCommands": { "defaults": { "requiresAuth": true } } }
Module exports override the settings default. Two ctx helpers close out the picture:
ctx.resolveSession() — resolve once without setting the module flag
ctx.saveSession() — persist a mutated ctx.session without importing SessionManager
Common Patterns
Create-then-verify
- name: create
command: resources create
args:
--name: "$name"
output: created
- name: verify
command: resources get
args:
--id: "$created.id"
assert: "$verify.name == $name"
Poll until ready
- name: wait
command: wait-until
args-positional:
- jobs status
args:
--id: "$job.id"
--timeout: "120"
Idempotent create (skip if exists)
- name: list-existing
command: projects list
output: projects
- name: create
command: projects create
args:
--name: "$name"
condition: "$_find($projects, name, $name) == undefined"
output: created
- name: verify
command: projects list
output: final
assert: "$_contains($final, name, $name) == true"
Batch operations
- name: process-all
forEach: "$list.items"
continueOnError: true
steps:
- name: process
command: items update
args:
--id: "$item.id"
--status: "processed"