Pipeline Reference¶
Forge pipelines are YAML or JSON files, typically stored at .forge/pipeline.yml in your repository root.
File Format¶
Pipelines can be written in either YAML (.yml or .yaml) or JSON (.json). YAML is recommended for human-authored files; JSON is useful when pipelines are generated programmatically.
name: my-pipeline # required — human-readable run name
steps:
- id: first-step
image: alpine:latest
run: echo "hello"
- id: second-step
image: alpine:latest
depends_on: [first-step]
run: echo "world"
Step Fields¶
Required Fields¶
| Field | Type | Description |
|---|---|---|
id |
string | Unique step identifier. Used in depends_on references and shown in the Web UI. |
image |
string | Docker image to run this step in. Not required for type: pipeline or type: approval steps. |
uses |
string | Path to a step template file. Reuses definition from another file. |
At least one of run, command, or script is required for non-pipeline steps.
Execution Fields¶
| Field | Type | Description |
|---|---|---|
run |
string | Shell command, passed to sh -c. Supports multiline with \|. |
command |
string[] | Explicit argv, bypasses shell. Use when you need exact argument control. |
script |
string | Path to a script file in the workspace. Interpreter inferred from extension. |
workdir |
string | Working directory inside the container. Default: /workspace. |
image |
string | Docker image. Pulled fresh if not cached locally. |
docker_socket |
bool | Mount host Docker socket into the container. Required for steps that run docker build or docker run. |
The script: Field¶
The script: field runs an external file from the workspace. The interpreter is inferred from the file extension:
| Extension | Interpreter |
|---|---|
.py |
python3 |
.sh, .bash |
sh |
.js, .mjs |
node |
.rb |
ruby |
.ts |
ts-node |
| (other) | sh |
- id: generate-matrix
type: generator
image: python:3.12-slim
script: scripts/ci/generate-matrix.py # runs python3 /workspace/scripts/ci/generate-matrix.py
Paths are relative to the workspace root. The script runs inside the container with the workspace mounted at /workspace.
Dependency and Flow Fields¶
| Field | Type | Description |
|---|---|---|
depends_on |
string[] | Step IDs this step must wait for. Forge computes the DAG and runs independent steps in parallel. |
inputs |
string[] | Glob patterns that affect this step's cache key. If undefined, caching is disabled. |
timeout |
string | Maximum duration. Parsed as Go duration: 5m, 1h30m, 45s. Default: 30 minutes. |
type |
string | Step type: task (default), generator, pipeline, or approval. |
condition |
string | CEL expression that must be true for the step to run (e.g., success(), failure()). |
always_run |
bool | Shorthand for condition: always(). Step runs even if dependencies fail. |
matrix |
map | Parallel execution with multiple values. Expands into multiple steps. |
Environment and Secrets¶
| Field | Type | Description |
|---|---|---|
env |
map | Environment variables injected into the container. |
secrets |
string[] | Secret names fetched from Vault and injected as env vars. Never stored in DB. |
Secret resolution order (highest priority first):
1. Project-scoped: secret set NAME value --project <id>
2. Org-scoped: secret set NAME value --org <id>
3. Global: secret set NAME value
4. Legacy path (backward compatibility)
Artifacts¶
artifacts:
upload:
- path: dist/myapp # path relative to workspace, glob patterns supported
name: app-binary # logical name for download (defaults to basename)
- path: dist/*.so # glob: each file uploaded with its basename as name
download:
- name: app-binary # logical name from a prior step's upload
dest: dist/myapp # destination path in workspace
- name: forge-* # wildcard: downloads all artifacts matching the pattern
dest: bin/ # destination directory
Artifacts are stored in the configured backend (local filesystem or S3-compatible) and shared across agents. A step on agent-1 can upload an artifact; a step on agent-2 can download it. Both upload and download support wildcard matching for managing groups of files.
Caching¶
Forge supports Content Addressable Storage (CAS) for pipeline steps. If a step's inputs haven't changed, Forge can skip execution and reuse the result from a previous run.
To enable caching, you must explicitly declare the files that affect the step using the inputs field:
- id: build-ui
image: node:22-alpine
inputs:
- ui/**
- package.json
- package-lock.json
run: |
cd ui
npm install
npm run build
The "task hash" for a step is computed from:
- The container image name
- The command and environment variables (see below)
- The SHA-256 hash of all files matched by the inputs globs
Determinism and Environment Variables¶
To ensure cache hits across different trigger events (e.g., a webhook push vs. a manual trigger), Forge excludes several non-deterministic environment variables from the task hash computation.
Excluded variables:
- FORGE_EVENT: The event type (e.g., push, pull_request, or empty for manual).
- FORGE_PR_NUMBER: The pull request number.
- FORGE_API_TOKEN: The authentication token used by the agent.
- FORGE_SCHEDULER_URL: The URL of the scheduler.
- FORGE_RUN_ID / FORGE_JOB_ID / FORGE_RUN_NAME: Unique identifiers for the current execution.
- FORGE_REPO_URL / FORGE_REPO_NAME: Repository metadata (which may differ in format between triggers).
All other environment variables, including FORGE_COMMIT_SHA, FORGE_BRANCH, and FORGE_COMMIT_TAG, are included in the hash. If your step depends on one of these variables changing, the cache will correctly miss.
If inputs is not defined, caching is disabled for that step, and it will always re-run.
Artifact Caching¶
Forge automatically caches and restores artifacts produced by a cached step. When a cache hit occurs, Forge identifies all artifacts that were uploaded in the original run and "bridges" them to the current run. This ensures that downstream steps that depend on those artifacts continue to work seamlessly even when the producer step is skipped.
Artifact Persistence
Artifact restoration depends on the original artifacts still being present in the storage backend. If the artifacts from the source run have been deleted (e.g., via a cleanup policy), Forge will discard the cache hit and re-run the step to regenerate them.
Advanced Features¶
Matrix Builds¶
Matrix builds allow you to run the same step multiple times with different variables. Forge expands the matrix at compile time into multiple distinct steps.
- id: test
image: node:${{ matrix.version }}
matrix:
version: [18, 20, 22]
os: [linux, macos]
run: |
echo "Running on ${{ matrix.os }} with Node ${{ matrix.version }}"
npm test
Variables are accessed via ${{ matrix.key }}. Forge generates step IDs like test-18-linux, test-20-linux, etc.
Any step that depends on a matrix step will automatically wait for all expanded instances of that matrix step to complete. Artifacts, environment variables, and release configurations within a matrix step also support ${{ matrix.key }} interpolation.
Step Templates (uses:)¶
Reusable step definitions can be stored in separate files and imported using the uses: field.
templates/docker-build.yml:
pipeline.yml:
Fields in the local step override fields in the template.
Manual Approvals¶
A step with type: approval will pause the pipeline and wait for a user to click "Approve" in the Web UI before downstream dependencies are unlocked.
- id: wait-for-approval
type: approval
depends_on: [test]
- id: deploy
image: alpine:latest
depends_on: [wait-for-approval]
run: ./deploy.sh
Conditional Execution¶
The condition field (aliased as if in some contexts, but condition in the internal IR) uses CEL expressions to determine if a step should run.
Supported functions:
- success(): All dependencies passed (default).
- failure(): At least one dependency failed.
- always(): Run regardless of dependency status.
- tag(): Only run if the pipeline was triggered by a Git tag.
- branch(name): Only run if the pipeline branch matches the given name or glob pattern. Supports multiple branches: branch(main, develop, feature/*).
- id: notify-failure
image: alpine:latest
condition: failure()
run: ./notify.sh "Build failed!"
- id: cleanup
image: alpine:latest
always_run: true # shorthand for condition: always()
run: rm -rf /tmp/build
Step Types¶
type: task (default)¶
A standard step that runs a command in a Docker container.
type: generator¶
A generator step runs code that emits new step definitions as a JSON array to stdout. Forge adds those steps to the current run and executes them. This enables runtime job generation — no static matrix required.
- id: matrix-generator
type: generator
image: python:3.12-slim
script: scripts/ci/generate-matrix.py
The script's stdout must be a valid JSON array of step definition objects. Stderr is captured as log output. Any subsequent step with depends_on: [matrix-generator] will wait for the generator AND all of its emitted children.
Generator script output format:
[
{
"id": "build-linux-amd64",
"image": "golang:1.24-alpine",
"depends_on": ["matrix-generator"],
"env": {"GOOS": "linux", "GOARCH": "amd64"},
"run": "go build -o dist/myapp-linux-amd64 ./cmd/myapp",
"artifacts": {
"upload": [{"path": "dist/myapp-linux-amd64", "name": "binary-linux-amd64"}]
}
}
]
type: pipeline¶
A pipeline step compiles and submits another pipeline as a child run. The agent acts as an orchestrator — no container is launched. Variables are injected as environment variables into every step of the child pipeline.
- id: deploy-staging
type: pipeline
pipeline: .forge/deploy.yml # path relative to workspace root
wait: true # block until child run completes (default: true)
depends_on: [build]
variables:
ENVIRONMENT: staging
REPLICAS: "3"
artifacts_send:
- container-image # artifact name from current run
artifacts_receive:
- deployed-endpoint # artifact from child run, added to current run
Pipeline step fields:
| Field | Type | Description |
|---|---|---|
pipeline |
string | Path to pipeline file, relative to workspace root. |
wait |
bool | If true (default), block until child run reaches a terminal state. |
variables |
map | Env vars injected into every step of the child pipeline, overriding the step's own env. |
artifacts_send |
string[] | Artifact names from the parent run copied to the child run's context before it starts. |
artifacts_receive |
string[] | Artifact names from the child run copied back into the parent run after the child completes. |
type: release¶
A release step pushes artifacts to an SCM provider (GitHub/GitLab). Unlike standard tasks, release steps run on the scheduler and do not require an agent. It automatically retrieves artifacts from the run's storage and uploads them as release assets.
- id: github-release
type: release
depends_on: [build-binaries]
condition: tag()
release:
name: "Release ${{ env.FORGE_COMMIT_TAG }}" # interpolated at runtime
tag: "${{ env.FORGE_COMMIT_TAG }}"
body: "Forge Release ${{ env.FORGE_COMMIT_TAG }}"
artifacts:
- forge-* # supports wildcards to upload multiple binaries
- checksums.txt
Release step fields:
| Field | Type | Description |
|---|---|---|
name |
string | The title of the release. Supports ${{ env.VAR }}. |
tag |
string | The Git tag to associate the release with. Supports ${{ env.VAR }}. |
body |
string | The description/notes for the release. Supports ${{ env.VAR }}. |
artifacts |
string[] | A list of artifact names or glob patterns to attach to the release. |
The release block supports interpolation for:
- ${{ env.FORGE_COMMIT_TAG }}: The Git tag that triggered the run.
- ${{ env.FORGE_BRANCH }}: The Git branch.
- ${{ env.FORGE_COMMIT_SHA }}: The full commit SHA.
Ensure your SCM token has sufficient permissions to create releases and upload assets.
Complete Example¶
name: build-test-deploy
steps:
# Parallel: test and lint run simultaneously
- id: test
image: golang:1.24-alpine
timeout: 10m
run: go test ./... -race -coverprofile=coverage.out
- id: lint
image: golangci/golangci-lint:latest
timeout: 5m
run: golangci-lint run ./...
# Build waits for both test AND lint
- id: build
image: golang:1.24-alpine
depends_on: [test, lint]
timeout: 10m
env:
CGO_ENABLED: "0"
GOOS: linux
run: go build -ldflags="-s -w" -o dist/app ./cmd/app
artifacts:
upload:
- path: dist/app
name: app-binary
# Containerize downloads the binary built above
- id: containerize
image: docker:27-cli
docker_socket: true
depends_on: [build]
timeout: 10m
artifacts:
download:
- name: app-binary
dest: dist/app
run: |
chmod +x dist/app
docker build -t myapp:${GIT_SHA:-dev} .
artifacts:
upload:
- path: image-digest.txt
name: image-digest
# Deploy to staging via a reusable child pipeline
- id: deploy-staging
type: pipeline
pipeline: .forge/deploy.yml
depends_on: [containerize]
variables:
ENVIRONMENT: staging
IMAGE_TAG: "${GIT_SHA:-dev}"
artifacts_send: [image-digest]
artifacts_receive: [staging-endpoint]
# Integration tests against the deployed staging environment
- id: integration-tests
image: python:3.12-slim
depends_on: [deploy-staging]
timeout: 15m
secrets: [STAGING_API_KEY]
artifacts:
download:
- name: staging-endpoint
dest: /tmp/endpoint.txt
run: |
pip install --quiet pytest httpx
ENDPOINT=$(cat /tmp/endpoint.txt)
pytest tests/integration/ --base-url="$ENDPOINT"
# Only deploy to production if integration tests pass
- id: deploy-production
type: pipeline
pipeline: .forge/deploy.yml
depends_on: [integration-tests]
variables:
ENVIRONMENT: production
IMAGE_TAG: "${GIT_SHA:-dev}"
artifacts_send: [image-digest]
YAML Tips¶
Multiline scripts¶
Inline sequences for dependencies and secrets¶
Environment variable interpolation¶
Forge does not interpolate ${} in pipeline YAML at compile time. Values like ${GIT_SHA:-dev} are passed literally to sh -c and expanded by the shell at runtime. This is intentional — it keeps pipeline files static and version-stable.
Validation¶
Validate a pipeline file without running it:
This catches syntax errors, missing required fields, circular dependencies, and unknown step types.