From apijack
Guides writing apijack YAML routines that chain CLI commands with variables, loops, conditions, assertions, randomization, and output capture for multi-step automations.
How this skill is triggered — by the user, by Claude, or both
Slash command
/apijack:write-routineThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Routines are YAML workflows that chain CLI commands. They live in `~/.<cli>/routines/` (or `.apijack/routines/` in project mode).
Routines are YAML workflows that chain CLI commands. They live in ~/.<cli>/routines/ (or .apijack/routines/ in project mode).
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.
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" }
# ... 48 more
Good (1 loop):
- name: create-todos
range: [1, 50]
as: n
steps:
- name: create
command: todos create
args:
--title: "Todo $n"
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).
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
Variables:
$_timestamp — Unix epoch seconds$_date — ISO date (YYYY-MM-DD)$_uuid — random UUIDRandomization 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: 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).
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.
- name: process-items
forEach: "$created.items"
as: item # default is "item" if omitted
steps:
- name: handle
command: items process
args:
--id: "$item.id"
Modifiers:
shuffle: true — randomize iteration orderreverse: 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"
- 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.
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.
- name: risky-step
command: might-fail
continueOnError: true
Call another routine inline:
- name: setup
command: routine run
args-positional:
- setup/environment
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
<cli> routine run <name> # Execute
<cli> routine run <name> --set key=value # Override variables
<cli> routine run <name> --dry-run # Preview without executing
<cli> routine validate <name> # Check YAML structure
<cli> routine test <name> # Run spec/test file
<cli> routine list # List available
<cli> routine list --tree # Show tree structure
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 {
// helpers.resolve() expands $refs and built-in functions inside the arg
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)" # → "HELLO-WORLD"
Rules:
export const name) becomes the function name. It must start with _ to match the $_* call syntax._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).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 |
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:
// .apijack/commands/sync.ts
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 () => {
// ctx: AuthedCliContext — ctx.session is non-null, no casts needed
});
};
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 flagctx.saveSession() — persist a mutated ctx.session without importing SessionManager- name: create
command: resources create
args:
--name: "$name"
output: created
- name: verify
command: resources get
args:
--id: "$created.id"
assert: "$verify.name == $name"
- name: wait
command: wait-until
args-positional:
- jobs status
args:
--id: "$job.id"
--timeout: "120"
- 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"
- name: process-all
forEach: "$list.items"
continueOnError: true
steps:
- name: process
command: items update
args:
--id: "$item.id"
--status: "processed"
Guides creation, editing, and verification of skills for AI coding agents using test-driven development with subagent scenarios. Use when authoring or debugging skills.
npx claudepluginhub normalled/apijack