Airlock
Guides

Custom Steps

Write reusable steps and produce artifacts from workflow steps.

Workflow steps can reference reusable step definitions using the uses keyword. You can also write inline steps with run that produce artifacts.

The uses Format

The uses field references a step definition from a Git repository or a local directory.

Remote Steps

Reference a step from a Git repository with owner/repo/path@ref:

- name: lint
  uses: airlock-hq/airlock/defaults/lint@main

This fetches the step definition from the defaults/lint directory of the airlock-hq/airlock repo at the main ref.

Local Steps

Reference a step from your repository with ./path (relative to the repo root):

- name: custom-lint
  uses: ./my-steps/lint

This resolves the step definition from the my-steps/lint directory in your repository. Airlock looks for step.yml, action.yml, or stage.yaml in the referenced directory. Path traversal outside the repository is blocked.

Local steps are useful for developing and testing custom steps before publishing them to a Git repository.

Writing a Custom Step

A custom step is a directory containing a step.yml file and any scripts it needs:

custom-lint/
  step.yml
  run.sh

The step.yml defines what the step does:

name: Custom Lint
run: bash run.sh
shell: bash

Reference it in your workflow — either from a local directory or a Git repository:

# Local step (during development)
- name: lint
  uses: ./custom-lint

# Remote step (after publishing)
- name: lint
  uses: my-org/my-steps/custom-lint@main

Environment Variables

Steps have access to environment variables like AIRLOCK_WORKTREE, AIRLOCK_BRANCH, and AIRLOCK_FROZEN that provide context about the current run.

Producing Artifacts

Custom steps can produce artifacts — content, comments, and patches — using the airlock artifact commands.

Example: Custom Review Step

Here's a complete custom step that runs a linter and turns each finding into a review comment:

# my-org/my-steps/review/step.yml
name: Custom Review
shell: bash
run: |
  set -euo pipefail

  # Run a custom linter that outputs JSON
  ./bin/lint --json > results.json

  # Loop through each finding and produce a comment artifact
  COUNT=$(airlock exec json 'findings | length' < results.json)
  for i in $(seq 0 $((COUNT - 1))); do
    FILE=$(airlock exec json "findings[$i].file" < results.json)
    LINE=$(airlock exec json "findings[$i].line" < results.json)
    MSG=$(airlock exec json "findings[$i].message" < results.json)
    SEV=$(airlock exec json "findings[$i].severity" < results.json)

    airlock artifact comment \
      --file "$FILE" \
      --line "$LINE" \
      --message "$MSG" \
      --severity "$SEV"
  done

Steps run in the worktree directory ($AIRLOCK_WORKTREE), so file paths in artifact commands should be relative to the repo root.