Skip to content
Back to skills

Write Routine

ASecurity

Use when building apijack YAML routines — workflow automations that chain CLI commands with variables, loops, conditions, and assertions

  • 7 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added May 27, 2026
toolsgobashexpressapi

Works with

  • cli
  • api

Security analysis

A100/100

Scanned May 27, 2026

npx -y skills add normalled/apijack --skill write-routine --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Write Routine?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Write Routine
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/normalled-write-routine/badge)](https://www.skillsdirectory.com/skills/normalled-write-routine)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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):
```yaml
- 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):
```yaml
- 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:

```json
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

```yaml
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:
```yaml
- 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:

```yaml
- 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:

```yaml
- 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

```yaml
- 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 order
- `reverse: true` — iterate in reverse order

```yaml
- 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

```yaml
- 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:

```yaml
- 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

```yaml
- name: risky-step
  command: might-fail
  continueOnError: true
```

## Sub-Routines

Call another routine inline:

```yaml
- name: setup
  command: routine run
  args-positional:
    - setup/environment
```

## Meta-Commands

Built-in commands available in routines:

- **`wait-until`** — poll until truthy result
  ```yaml
  - name: wait-ready
    command: wait-until
    args-positional:
      - resources get
    args:
      --id: "$created.id"
      --timeout: "60"
      --interval: "5"
  ```

- **`session refresh`** — re-authenticate mid-routine
  ```yaml
  - name: refresh
    command: session refresh
  ```

## Routine Commands

```bash
<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
```

## 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`:**
```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:**
```yaml
variables:
  greeting: "hello-world"
steps:
  - name: create
    command: resources create
    args:
      --title: "$_uppercase($greeting)"   # → "HELLO-WORLD"
```

**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:

```ts
// .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`:

```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

```yaml
- 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

```yaml
- name: wait
  command: wait-until
  args-positional:
    - jobs status
  args:
    --id: "$job.id"
    --timeout: "120"
```

### Idempotent create (skip if exists)

```yaml
- 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

```yaml
- name: process-all
  forEach: "$list.items"
  continueOnError: true
  steps:
    - name: process
      command: items update
      args:
        --id: "$item.id"
        --status: "processed"
```

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.