Ignition
Guides

Writing Recipes

Recipe patterns, variables, composition, and error handling.

Recipes are TypeScript files that describe the desired state of a remote host. They default-export an async function that receives an ExecutionContext.

Recipe format

import type { ExecutionContext } from "@grovemotorco/ignition"
import { createResources } from "@grovemotorco/ignition"

export default async function (ctx: ExecutionContext) {
  const { apt, exec, file, directory, service } = createResources(ctx)

  await apt({ name: "nginx", state: "present" })
  await service({ name: "nginx", state: "started" })
}

The createResources(ctx) factory returns all five built-in resources bound to the current context. Destructure the ones you need.

Resource ordering

Resources execute sequentially in the order you write them. This is by design — automation steps often depend on previous ones (install a package before configuring it, write a config file before restarting the service).

await apt({ name: "nginx", state: "present" }) // 1. install
await file({ path: "/etc/nginx/app.conf", content }) // 2. configure
await service({ name: "nginx", state: "restarted" }) // 3. restart

Variables

The execution context carries variables merged from multiple sources. Access them via ctx.vars:

export default async function (ctx: ExecutionContext) {
  const { file } = createResources(ctx)

  const domain = (ctx.vars.domain as string) ?? "localhost"

  await file({
    path: "/etc/nginx/sites-available/app.conf",
    content: `server_name ${domain};`,
  })
}

At recipe start, variables are merged with highest precedence first:

  1. --var CLI flags
  2. Config file vars
  3. Host-level vars (in inventory)
  4. Group-level vars (in inventory)
  5. Global inventory vars

Setting variables

Write to the current scope with setVar():

ctx.setVar("app_version", "2.1.0")

Scoped variables

Use withVars() to create a temporary scope. Variables set inside the scope don't leak to the outer context:

await ctx.withVars({ port: 8080 }, async () => {
  const port = ctx.vars.port // 8080
  await file({ path: "/etc/app.conf", content: `port=${port}` })
})
// ctx.vars.port is back to its previous value (or undefined)

Conditional logic

Use standard TypeScript control flow:

export default async function (ctx: ExecutionContext) {
  const { apt, exec, service } = createResources(ctx)

  if (ctx.vars.install_monitoring) {
    await apt({ name: "prometheus-node-exporter", state: "present" })
    await service({ name: "prometheus-node-exporter", state: "started" })
  }

  // Use host facts for platform-specific logic
  if (ctx.facts?.arch === "aarch64") {
    await exec({ command: "echo 'Running on ARM'" })
  }
}

Recipe metadata

Export a meta object to add a description and tags:

export const meta = {
  description: "Deploy the web application",
  tags: ["web", "deploy"] as const,
}

export default async function (ctx: ExecutionContext) {
  // ...
}

Filter recipes by the recipe module's meta.tags with repeatable --tags flags:

ignition run deploy.ts @web --inventory hosts.ts --tags web --tags deploy

When --tags is set, the recipe only runs if its tags overlap with the selected tags. This is currently recipe-level filtering only; per-resource call metadata tags are not wired into ignition run.

Composition

Extract shared logic into functions and import them across recipes:

lib/common.ts
import type { ExecutionContext } from "@grovemotorco/ignition"
import { createResources } from "@grovemotorco/ignition"

export async function hardenSSH(ctx: ExecutionContext) {
  const { file, service } = createResources(ctx)
  await file({
    path: "/etc/ssh/sshd_config.d/hardening.conf",
    content: "PermitRootLogin no\nPasswordAuthentication no\n",
    mode: "0600",
  })
  await service({ name: "sshd", state: "restarted" })
}
deploy.ts
import { hardenSSH } from "./lib/common.ts"

export default async function (ctx: ExecutionContext) {
  const { apt } = createResources(ctx)
  await apt({ name: "nginx", state: "present" })
  await hardenSSH(ctx)
}

Error handling

By default, Ignition uses fail-fast mode — the first resource failure stops execution. You can catch errors in your recipe:

export default async function (ctx: ExecutionContext) {
  const { exec } = createResources(ctx)

  try {
    await exec({ command: "some-risky-command" })
  } catch (err) {
    await exec({ command: "echo 'Fallback command'" })
  }
}

For broader control, use --error-mode:

# Run everything, report failures at the end
ignition run deploy.ts @web --error-mode fail-at-end

# Log failures and keep going
ignition run deploy.ts @web --error-mode ignore

See Error Handling for details on error modes, retries, and timeouts.

On this page