DS DevShelfHub Projects · AI tools
Cheatsheets / CircleCI
Cheatsheet · Dev tooling

CircleCI Cheatsheet: config.yml, Orbs and Workflows Reference

By DevShelfHub

config.yml v2.1, orbs, executors, workflows + DAG, caches + workspaces, parallelism + test splitting, contexts + OIDC, dynamic config, approval gates.

81 items 9 min Orbs Workflows Contexts

Start hereQuick start · 6 you’ll reach for daily

Config file.circleci/config.yml
Headerversion: 2.1
Orborbs: { python: circleci/python@2 }
Workflowworkflows: ci: { jobs: [test] }
Local runcircleci local execute --job test
Approvaltype: approval

Target versions · paceVersions

Targets: config version: 2.1 circleci CLI ≥ 0.1.30000 cimg/* convenience images

The cloud config is 2.1 — pin every file with that header to unlock orbs, reusable commands, parameters, and dynamic config. Recent additions: setup: true dynamic configs, OIDC tokens for cloud auth, ARM resource classes, the cimg/* base image family. Avoid the legacy version: 2 dialect — it lacks orbs and parameters.

config.yml · CLISetup

bash
# 1. Minimal .circleci/config.yml at repo root
mkdir -p .circleci
cat > .circleci/config.yml <<'YAML'
version: 2.1

jobs:
    test:
        docker: [{ image: cimg/python:3.12 }]
        steps:
            - checkout
            - run: pip install -r requirements.txt
            - run: pytest -q

workflows:
    ci:
        jobs: [test]
YAML

git add .circleci/config.yml && git commit -m "ci: enable circleci" && git push

# 2. CLI — local validation + local job runs
brew install circleci                      # macOS
# Or: curl -fLSs https://raw.githubusercontent.com/CircleCI-Public/circleci-cli/main/install.sh | bash

circleci version
circleci setup                             # interactive token setup

# 3. Validate config without pushing
circleci config validate                   # syntax + semantic check
circleci config process .circleci/config.yml   # show the expanded YAML (after orbs)

# 4. Run a single job locally (in Docker)
circleci local execute --job test

# 5. Connect a repo
#    Web UI: app.circleci.com → Projects → Set Up Project

Top-level keysconfig.yml structure

version: 2.1Required first line. Unlocks orbs / commands / parameters.
orbs:Pull in reusable packages of jobs / commands / executors.
executors:Named executor definitions reused by multiple jobs.
commands:Reusable step sequences with parameters.
parameters:Pipeline-level parameters — typed, surfaced in the UI.
jobs:Job definitions. Each job has an executor + steps.
workflows:Orchestrate jobs: order, fan-out, approvals, schedules.
setup: trueMarks this as a setup config — emit the real one via the continuation orb.

Steps · workspacesJobs

jobs.test.docker: [{ image: cimg/python:3.12 }]Docker executor — the most common.
jobs.test.steps: [...]Ordered list of steps. Run in the same container by default.
- checkoutBuilt-in step: clone the repo into the working dir.
- run: pytest -qInline shell command. The job’s working dir is the cwd.
- run: { name: install, command: pip install -r req.txt }Named step (better logs).
- run: { ..., when: on_fail }Conditional execution. when: always · on_success · on_fail.
- save_cache: { key: ..., paths: [...] }Cache write. Immutable per key.
- restore_cache: { keys: [...] }Cache read with fallback list.
- store_artifacts: { path: build/, destination: build }Upload as artifacts. Browseable in the UI.
- store_test_results: { path: junit/ }Test report parsing. Powers the Tests tab + timing splits.
- persist_to_workspace: { root: ., paths: [dist/] }Ephemeral storage shared across jobs in one pipeline.
- attach_workspace: { at: . }Pull the upstream job’s workspace.
- setup_remote_dockerProvision a remote Docker host (DinD). Required for building images inside a docker executor.
- when: { condition: ..., steps: [...] }Conditional block of steps based on a parameter / expression.
yaml
# Full config.yml — parameters, named executor, reusable command, two jobs, a workflow.
version: 2.1

parameters:
    deploy-env:
        type: enum
        enum: [staging, production]
        default: staging

executors:
    py:
        docker: [{ image: cimg/python:3.12 }]
        resource_class: medium
        working_directory: ~/repo

commands:
    install-deps:
        parameters:
            requirements:
                type: string
                default: requirements.txt
        steps:
            - restore_cache:
                  keys:
                      - v2-pip-{{ checksum "<< parameters.requirements >>" }}
                      - v2-pip-
            - run: pip install --user -r << parameters.requirements >>
            - save_cache:
                  key:   v2-pip-{{ checksum "<< parameters.requirements >>" }}
                  paths: [~/.cache/pip]

jobs:
    test:
        executor: py
        parallelism: 4
        steps:
            - checkout
            - install-deps
            - run:
                  name: run pytest with shard splitting
                  command: |
                      TESTS=$(circleci tests glob "tests/**/test_*.py" | \
                              circleci tests split --split-by=timings)
                      pytest --junitxml=junit/${CIRCLE_NODE_INDEX}.xml $TESTS
            - store_test_results: { path: junit }

    deploy:
        executor: py
        steps:
            - checkout
            - run: ./scripts/deploy.sh << pipeline.parameters.deploy-env >>

workflows:
    ci:
        jobs:
            - test
            - deploy:
                  requires: [test]
                  filters: { branches: { only: main } }

docker · machine · macos · windowsExecutors

docker: [{ image: cimg/python:3.12 }]Container. First image runs the steps; the rest are services.
docker: [{ image: ... }, { image: postgres:16 }]Side-car services available at localhost.
machine: { image: ubuntu-2204:current }Full Linux VM — root, kernel, real Docker daemon.
macos: { xcode: "15.4.0" }Real macOS host. Required for iOS / Mac builds.
windows: { default: server-2022 }Windows Server executor.
resource_class: small / medium / large / xlarge / 2xlargeCPU + RAM tier. Larger = more credits / minute.
resource_class: arm.mediumARM64 runner.
working_directory: ~/repoCwd for the job. Defaults to ~/project.

DAG · filters · approvals · cronWorkflows

workflows.ci.jobs: [test, deploy]Sequential by default (each job in its own slot, kicked off in order).
- deploy: { requires: [test, lint] }DAG dependency — deploy waits for both upstream jobs.
- deploy: { filters: { branches: { only: main } } }Branch filter (string or regex list).
- deploy: { filters: { tags: { only: /^v.*/ } } }Tag filter. Tags only run if explicitly allowed.
- hold: { type: approval }Manual approval gate — appears as a button in the workflow graph.
- deploy: { requires: [hold] }Gate downstream on the approval job.
triggers: [{ schedule: { cron: "0 4 * * *", filters: {...} } }]Cron-style scheduled runs.
when: { equal: [main, << pipeline.git.branch >>] }Conditional workflow — runs only when the expression evaluates true.
unless: { ... }Inverse of when.
filters.branches.ignore: [main, develop]Negative filter — everything except listed.

Reusable packagesOrbs

orbs: { python: circleci/python@2.1 }Import an orb. Pin minor for stable behaviour.
python/install-packages: { pkg-manager: pip }Orb-provided command. Caches deps automatically.
executor: python/defaultOrb-provided executor.
aws-cli/setup: { role-arn: ... }OIDC AWS auth via the aws-cli orb.
gcp-cli/installGCP equivalent.
slack/notify: { event: fail, channel: ci-alerts }Send a Slack message on success / fail.
docker/build / docker/pushImage build + push wrappers.
circleci orb create namespace/nameAuthor + publish your own orb to the registry.
yaml
# Orbs are versioned packages of reusable jobs / commands / executors.
version: 2.1

orbs:
    python:  circleci/python@2.1                  # CircleCI-maintained
    aws-cli: circleci/aws-cli@4.1
    slack:   circleci/slack@4.13
    docker:  circleci/docker@2.5

jobs:
    test:
        executor: python/default                  # orb-provided executor
        steps:
            - checkout
            - python/install-packages:            # orb-provided command
                  pkg-manager: pip
                  cache-version: v2
            - run: pytest -q
            - slack/notify:
                  event: fail
                  channel: ci-alerts

    publish:
        docker: [{ image: cimg/base:current }]
        steps:
            - checkout
            - setup_remote_docker: { docker_layer_caching: true }
            - aws-cli/setup:
                  role-arn:    arn:aws:iam::123:role/gh-oidc
                  role-session-name: circleci-${CIRCLE_BUILD_NUM}
            - docker/check
            - docker/build:
                  image: my-org/app
                  tag:   ${CIRCLE_SHA1}
            - docker/push:
                  image: my-org/app
                  tag:   ${CIRCLE_SHA1}

workflows:
    ci:
        jobs:
            - test
            - publish:
                  context: aws-prod                # context = org-level secret bundle
                  requires: [test]
                  filters: { branches: { only: main } }

save_cache · restore_cache · workspacesCaching

save_cache: { key: v2-pip-{{ checksum "requirements.txt" }}, paths: [.venv/] }Key by file checksum — busts when deps change.
restore_cache: { keys: [v2-pip-{{ checksum "..." }}, v2-pip-] }Try exact key, then fall back to the prefix. First match wins.
v1- → v2-Bump the prefix to force a fresh cache. Caches are immutable per key.
{{ .Branch }} · {{ .Revision }} · {{ epoch }} · {{ arch }}Built-in template vars for cache keys.
setup_remote_docker: { docker_layer_caching: true }DLC for image builds. Plan-gated; speeds up repeated docker build.
persist_to_workspace: { root: ., paths: [dist/] }Per-pipeline ephemeral storage. Free, pipeline-scoped.
attach_workspace: { at: . }Receive a workspace in a downstream job.
Cache vs workspaceCache = cross-pipeline, immutable per key. Workspace = one pipeline only, mutable.
yaml
# Caches are immutable per key. Fall through with a prefix when the exact key misses.
# Bump the version prefix (v2- → v3-) when the cache shape changes.
jobs:
    build:
        docker: [{ image: cimg/node:20.11 }]
        steps:
            - checkout

            - restore_cache:
                  keys:
                      - v3-node-{{ arch }}-{{ checksum "package-lock.json" }}
                      - v3-node-{{ arch }}-                                    # prefix fallback

            - run: npm ci --prefer-offline

            - save_cache:
                  key:   v3-node-{{ arch }}-{{ checksum "package-lock.json" }}
                  paths: [node_modules, ~/.npm]

            - run: npm run build

            # Pass artifacts between jobs in the same pipeline (ephemeral)
            - persist_to_workspace:
                  root: .
                  paths: [dist/, package.json]

    deploy:
        docker: [{ image: cimg/node:20.11 }]
        steps:
            - attach_workspace: { at: . }                                       # pulls from build
            - run: npx --yes wrangler deploy

# Docker Layer Caching for image builds
# (requires the "Docker Layer Caching" feature on your plan):
#   - setup_remote_docker:
#         docker_layer_caching: true
#   - run: docker build -t my-app .

Test splittingParallelism & splitting

parallelism: 6Run 6 parallel containers of the same job.
$CIRCLE_NODE_INDEX · $CIRCLE_NODE_TOTAL0-based shard index + total shard count.
circleci tests glob "test/**/*.py" | circleci tests split --split-by=timingsPreferred Split by historical timings.
circleci tests split --split-by=filesizeFallback split when no timing data yet.
--timings-type=classnameMatch timings on class name — useful when test files contain multiple test classes.
store_test_results paired with parallelismEach shard’s junit output feeds future splits. Without it, splitting degrades to filename hashing.
yaml
# CircleCI splits tests across N containers using past timing data.
# Upload junit reports → next run's split has accurate timings.
jobs:
    test:
        docker: [{ image: cimg/python:3.12 }]
        parallelism: 6                              # number of parallel containers
        steps:
            - checkout
            - run: pip install -r requirements.txt

            - run:
                  name: shard tests by historical timing
                  command: |
                      # Each container picks a different shard
                      TESTS=$(circleci tests glob "tests/**/test_*.py" \
                              | circleci tests split --split-by=timings --timings-type=classname)
                      mkdir -p test-results
                      pytest --junitxml=test-results/junit-${CIRCLE_NODE_INDEX}.xml $TESTS

            - store_test_results: { path: test-results }     # feeds future timing splits

            - run:
                  name: e2e (only on the first container)
                  command: ./scripts/e2e.sh
                  when: always
                  # Skip on non-zero CIRCLE_NODE_INDEX
                  # by using a conditional shell test:
                  # [ "$CIRCLE_NODE_INDEX" -ne 0 ] && circleci-agent step halt

Org-level secret bundlesContexts & secrets

Context (org-level)Named bundle of env vars. Restrict access by Group.
jobs.deploy: { context: aws-prod }Inject a single context into a job.
context: [aws-prod, slack-token]Multiple contexts. Later wins on collision.
Project env varsPer-project, set in the project’s settings → Environment Variables.
OIDC via aws-cli orbExchange CircleCI’s JWT for short-lived AWS creds. No static secret.
Restricted contexts (Security)Limit which Groups / pipelines can attach a given context.

setup workflow · continuationDynamic config

setup: trueTop-level flag: this config emits the real one rather than running jobs directly.
orbs: { continuation: circleci/continuation@1 }Continuation orb — takes the generated config and runs it.
continuation/continue: { configuration_path: ... }Trigger the second pipeline. Pass parameters too.
orbs: { path-filtering: circleci/path-filtering@1 }Diff changed paths and emit parameters — runs only the affected packages.
path-filtering/filter: { mapping: ... }Map changed-path globs to parameter values.

type: approval · branches · tagsApprovals & filters

type: approvalA no-op job that becomes a manual gate in the workflow graph.
filters: { branches: { only: main } }String, list, or regex.
filters: { branches: { ignore: [main, develop] } }Negative filter — everything except.
filters: { tags: { only: /^v\d+/ } }Regex match. Tags must be explicitly allowed.
requires + filtersCombine: only deploy after upstream succeeds and only from main.

Full pipeline · ~45 linesEnd-to-end · Node app

Lint → parallel test (with timing-based splitting) → build → manual approval → deploy with Slack notification, using the official Node + Slack orbs.

yaml
# End-to-end pipeline: lint → test (parallel) → build → manual approval → deploy.
version: 2.1

orbs:
    node:  circleci/node@5.2
    slack: circleci/slack@4.13

executors:
    node-20:
        docker: [{ image: cimg/node:20.11 }]
        resource_class: medium

jobs:
    lint:
        executor: node-20
        steps:
            - checkout
            - node/install-packages: { cache-version: v3 }
            - run: npm run lint

    test:
        executor: node-20
        parallelism: 4
        steps:
            - checkout
            - node/install-packages: { cache-version: v3 }
            - run:
                  command: |
                      TESTS=$(circleci tests glob "test/**/*.spec.ts" \
                              | circleci tests split --split-by=timings)
                      npx jest --reporters=jest-junit $TESTS
            - store_test_results: { path: junit }

    build:
        executor: node-20
        steps:
            - checkout
            - node/install-packages: { cache-version: v3 }
            - run: npm run build
            - persist_to_workspace: { root: ., paths: [dist/] }

    deploy:
        executor: node-20
        steps:
            - attach_workspace: { at: . }
            - run: ./scripts/deploy.sh
            - slack/notify: { event: pass, channel: deploys }

workflows:
    ci:
        jobs:
            - lint
            - test
            - build:  { requires: [lint, test] }
            - hold:   { type: approval, requires: [build], filters: { branches: { only: main } } }
            - deploy: { context: aws-prod, requires: [hold] }

Best practiceGood to know

Always pair parallelism with store_test_results. Without junit data the splitter falls back to filename hashing — shards drift and one container ends up with all the slow tests. Upload junit-${CIRCLE_NODE_INDEX}.xml per shard and the next run distributes evenly by historical timing.
Bake the version prefix into every cache key. Caches are immutable, so v1-... becomes a dead branch once you change shape. Starting with v1- means you can bump to v2- in one commit and force a fresh cache without renaming the project.
Use orbs for cloud auth, not static keys. circleci/aws-cli + circleci/gcp-cli both support OIDC. You declare a role ARN, CircleCI hands the role a JWT, the role hands back short-lived creds. No AWS_ACCESS_KEY_ID in project settings to rotate or leak.

Common trapsWatch out for

Caches are immutable per key — you can’t patch one. Once written, a key’s value is fixed forever. The fallback prefix in restore_cache.keys is how you migrate cleanly. Forgetting that turns "we updated the cache" into "the new step never runs".
Tag triggers don’t fire on default branch filters. A workflow with no tags: filter ignores tag pushes entirely. If you want a tag-only release pipeline, you must set filters.tags.only and filters.branches.ignore: /.*/ on the job.
Orbs drift between minor versions. circleci/python@2 follows the latest 2.x — behaviour can change on push from the orb publisher. Pin to @2.1 or exact @2.1.4 for reproducible pipelines.

Go deeperSee also

CircleCI FAQ

What is an orb in CircleCI?

An orb is a reusable, shareable package of CircleCI configuration that bundles jobs, commands, and executors. First-party orbs like circleci/python or circleci/node let you add common steps in one line instead of duplicating YAML across projects.

What is the difference between a cache and a workspace in CircleCI?

Caches persist data across pipeline runs and are keyed for reuse (e.g., dependency caches). Workspaces are ephemeral within a single pipeline and pass files between jobs in the same workflow, such as handing build artifacts to a deploy job.

How does CircleCI parallelism work?

Set parallelism: N on a job to spin up N containers running the same steps concurrently. Use circleci tests split to divide test files across containers by timing data, then combine results with store_test_results so CircleCI merges the JUnit output.

What is a CircleCI context?

A context is a named group of environment variables stored in your org settings and injected into jobs at runtime. Assign context: [my-context] in the workflow block. OIDC token injection lets jobs authenticate to cloud providers without storing long-lived secrets.

What is dynamic config in CircleCI?

Dynamic config lets a setup workflow generate the real pipeline config at runtime instead of committing a static file. Mark the file with setup: true, use the continuation orb to emit the generated config, and CircleCI will execute that second config as the main pipeline.

How do I run CircleCI locally?

Install the CircleCI CLI (brew install circleci or the Linux installer) and run circleci local execute --job <job-name>. This requires Docker and runs your job inside a local container, which is useful for debugging before pushing to the cloud.