CircleCI Dynamic Config + Tag Pipelines: Why You’re Getting ‘No Workflow’ and How to Fix It

The Error That Wastes Half Your Afternoon

The worst CI failure mode isn’t a red build β€” it’s a build that looks like it never existed. You push a tag like v2.4.1, watch the CircleCI dashboard for a few seconds, and see… nothing. Not a failed pipeline. Not a warning. Just the tag sitting there, completely ignored. You refresh. Still nothing. You check the project settings, the webhook logs, and start wondering if you accidentally broke something fundamental. That half-afternoon is already gone.

What makes this especially painful with dynamic config is the failure happens in a layer before your real config even loads. CircleCI’s dynamic config feature works by running a setup pipeline first β€” a small .circleci/config.yml that calls circleci/continuation to hand off to your actual workflow logic. If anything goes wrong during that continuation step (wrong config path, malformed generated YAML, a parameter mismatch), CircleCI swallows the error and reports zero workflows ran. No stack trace. No failure log you can click into. The setup pipeline shows green because it ran fine β€” it just didn’t successfully launch anything downstream.

# This is your setup config. It runs. It looks fine. It lies.
version: 2.1

setup: true

orbs:
  continuation: circleci/[email protected]

jobs:
  generate-config:
    docker:
      - image: cimg/python:3.11
    steps:
      - checkout
      - run:
          name: Generate dynamic config
          command: python scripts/generate_config.py > /tmp/generated_config.yml
      - continuation/continue:
          configuration_path: /tmp/generated_config.yml
          # If generated_config.yml is invalid YAML or has no workflows
          # that match the current pipeline parameters, you get silence.

Two scenarios cause this more than anything else. First: tag-only release pipelines where you filter on tags in your workflow but forget that CircleCI’s default behavior is to not run workflows for tags at all unless explicitly told to. Your generated config needs tags filter blocks on every job in the workflow, including jobs that have nothing to do with tagging. Miss one job, and the whole workflow is silently skipped. Second: monorepo path filtering combined with version tags. You use something like circleci-config-sdk or a custom script to generate workflows only for changed paths. A git tag doesn’t change any files β€” so your path-change detection script outputs a config with zero workflows, the continuation runs successfully with that empty config, and CircleCI shrugs and moves on.

# The tag filter must appear on EVERY job in the workflow, not just the trigger
workflows:
  release:
    jobs:
      - build:
          filters:
            tags:
              only: /^v.*/
            branches:
              ignore: /.*/
      - deploy:
          requires:
            - build
          filters:
            tags:
              only: /^v.*/   # miss this and 'deploy' never runs, silently
            branches:
              ignore: /.*/

The monorepo case is trickier because the fix isn’t just adding tag filters β€” it’s making your config generation script aware that a tag push is a special case that should bypass path diffing entirely. Check CIRCLE_TAG in the environment at generation time. If it’s set, skip the diff logic and emit the full release workflow regardless of what files changed. That single env var check has saved me from this exact silent failure more than once. For a complete list of tools that fit into a CI/CD-first workflow, check out our guide on Productivity Workflows.

Quick Background: How Dynamic Config Actually Works (and Where It Can Break)

The thing that trips people up most is that dynamic config isn’t one pipeline with a conditional β€” it’s literally two separate pipeline executions. Your .circleci/config.yml with setup: true is the first pipeline. Its only job is to figure out what should run next and call the continuation orb. The continuation orb then fires a completely separate pipeline using a different config file you specify at runtime. These two pipelines show up as separate entries in your dashboard, have separate pipeline IDs, and can fail independently. I missed this for an embarrassingly long time, wondering why my “main” pipeline wasn’t showing the jobs I expected β€” they were in a completely different pipeline entry, sometimes on the next page.

setup: true does more than flip a boolean. When CircleCI sees that flag, it validates and executes your config file differently β€” it tells the platform to expect a continuation call before considering the pipeline complete. Without it, CircleCI treats your config as a normal pipeline and any attempt to call the continuation orb will fail with auth errors because CIRCLE_CONTINUATION_KEY is never injected. That key is a short-lived token, generated per-pipeline-run, that authenticates your continuation call. CircleCI only generates and injects it when setup: true is present. No flag, no key, no second pipeline.

# Minimal working setup pipeline
version: 2.1
setup: true  # This line changes everything about how CircleCI processes this file

orbs:
  continuation: circleci/[email protected]  # pin the version β€” latest can break you silently

workflows:
  setup-workflow:
    jobs:
      - decide-config:
          filters:
            tags:
              only: /^v.*/

jobs:
  decide-config:
    docker:
      - image: cimg/base:stable
    steps:
      - checkout
      - run:
          name: Generate pipeline parameters
          command: |
            # Build your parameters JSON β€” must be valid JSON, even if empty
            echo '{"deploy_env": "production", "run_integration": true}' > /tmp/pipeline-params.json
      - continuation/continue:
          configuration_path: .circleci/continue_config.yml  # path relative to repo root
          parameters: /tmp/pipeline-params.json

The continuation orb needs three things to work: the CIRCLE_CONTINUATION_KEY (auto-injected, you don’t set this), a valid path to your continuation config, and a parameters payload that is both valid JSON and matches the parameter declarations in your continuation config file. The third one is where most silent failures happen. If your continuation config declares deploy_env as a string parameter but you pass it as an integer, or if you pass a parameter key that isn’t declared at all, the API call fails. But here’s the nasty part β€” depending on the orb version, this can fail without a clear error message in the setup pipeline’s output. The setup pipeline shows green, the continuation fires, and then you get “no workflow” on the second pipeline because the parameter mismatch caused it to receive a malformed config context.

The four places the hand-off silently dies, from most to least obvious:

  • Wrong config path: configuration_path: .circleci/continue_config.yml is relative to the repo root after checkout. If the file doesn’t exist at that exact path, you’ll get an error β€” but only if the orb version you’re using surfaces it. Older versions of circleci/[email protected] would swallow this and produce a confusing downstream error.
  • Malformed parameters JSON: Trailing commas, unquoted keys, passing a file path instead of actual JSON content to the parameters field β€” all will silently skip your workflows. Always validate with jq empty /tmp/pipeline-params.json before calling continue.
  • Parameter schema mismatch: Your continuation config must declare every parameter you pass, with matching types. Extra parameters not declared in the config are rejected by the API.
  • Orb version mismatch: The [email protected] and [email protected] orbs have different parameter field names. Mixing documentation from one with the actual orb version you pinned produces jobs that look like they ran but triggered nothing.

Setting Up the Baseline: Your config.yml and continue_config.yml

The thing that trips most people up isn’t the concept of dynamic config β€” it’s that CircleCI expects a very specific file structure and any deviation from it produces the world’s least helpful error: “no workflow”. Before you touch tag filters or parameter passing, get the directory layout exactly right.

Directory Structure That Actually Works

CircleCI looks for exactly two files when dynamic config is enabled on your project:

.circleci/
  config.yml          # The setup config β€” this is your entrypoint
  continue_config.yml # The continuation config β€” this runs the real pipelines

config.yml is what CircleCI processes first. Its only job is to evaluate conditions and then hand off to the continuation config. continue_config.yml doesn’t have to live at that path β€” you can generate it dynamically and pass an arbitrary path to the orb β€” but defaulting to that location keeps things sane. If you start generating config files on the fly and storing them in /tmp or a workspace, you’ll spend more time debugging path issues than the flexibility is worth. Stick with the static path until you genuinely need generated configs.

Minimal Setup Config That Won’t Lie to You

Here’s the smallest config.yml that actually works end-to-end. Notice setup: true at the top level β€” without it, CircleCI treats this as a regular config and ignores your continuation orb call entirely:

version: 2.1
setup: true  # this line is the entire mechanism β€” drop it and nothing works

orbs:
  continuation: circleci/[email protected]  # pinned, not @1

jobs:
  setup:
    docker:
      - image: cimg/base:2024.01
    steps:
      - checkout
      - continuation/continue:
          configuration_path: .circleci/continue_config.yml

workflows:
  setup-workflow:
    jobs:
      - setup

That’s it. No conditions yet, no parameter passing. Run this first and confirm the continuation fires before you add any complexity. The cimg/base:2024.01 image is fine here β€” your setup job doesn’t need anything heavy because it’s not building code, just evaluating conditions and calling the orb.

Passing pipeline.git.tag as a Parameter

This is where the “no workflow” error usually strikes for tag-based pipelines. The continuation orb lets you pass parameters to the continuation config, but the syntax has a gotcha: parameters must be JSON-encoded strings in the parameters field. Here’s a working setup that forwards the git tag:

version: 2.1
setup: true

orbs:
  continuation: circleci/[email protected]

jobs:
  setup:
    docker:
      - image: cimg/base:2024.01
    steps:
      - checkout
      - continuation/continue:
          configuration_path: .circleci/continue_config.yml
          # parameters must be a JSON string β€” not YAML, not a map
          parameters: '{"git_tag": "<< pipeline.git.tag >>"}'

workflows:
  setup-workflow:
    jobs:
      - setup

And in your continue_config.yml, you receive it like this:

version: 2.1

parameters:
  git_tag:
    type: string
    default: ""  # empty string when not a tag build

jobs:
  release:
    docker:
      - image: cimg/base:2024.01
    steps:
      - run: echo "Building release for tag << parameters.git_tag >>"

workflows:
  release-workflow:
    when:
      not:
        equal: ["", << parameters.git_tag >>]
    jobs:
      - release

The when condition on the workflow is what prevents a “no workflow” error on non-tag pushes. If git_tag is empty and you have no other workflows defined, CircleCI will complain. Always have a fallback workflow or guard every workflow with a condition that covers the empty case.

Pin the Orb Version β€” @1 Will Eventually Burn You

Using circleci/continuation@1 (the floating major version tag) versus circleci/[email protected] feels like a minor style choice but it isn’t. CircleCI orb versioning follows semver, but “minor” orb releases can change default parameter behavior, add required fields, or alter how the continuation API call is constructed under the hood. I saw a pipeline that had been green for months suddenly start producing malformed continuation API requests after an orb patch release changed how it URL-encoded the parameters field.

The fix is one line: pin to a specific version. Check the CircleCI orb registry for the current stable release and use that exact version string. When you want to upgrade, do it deliberately with a PR and test it against a branch pipeline first. The @1 floating tag exists for convenience, but convenience in CI config is how you get a 3am page about a deploy pipeline that stopped working for no apparent reason.

The Tag Pipeline Problem Specifically

The thing that caught me off guard the first time I set up tag-triggered dynamic config: the “No Workflow” error doesn’t mean your downstream config is broken. It means your setup job never ran. CircleCI evaluates tag filters on every config in the chain independently, so if your .circleci/config.yml setup job doesn’t explicitly allow tags, the pipeline sees a tag push, finds no matching workflow trigger in the setup config, and bails out entirely. The continuation API never gets called. Your generated config is irrelevant at that point.

The specific filter block you need on your setup job looks like this β€” and the branches: ignore: /.*/ line is not optional if you only want this to fire on tags:

# .circleci/config.yml (the setup config)
version: 2.1

setup: true

orbs:
  continuation: circleci/[email protected]

workflows:
  setup-workflow:
    jobs:
      - generate-config:
          filters:
            tags:
              only: /^v.*/       # must be here or tag pipelines die immediately
            branches:
              ignore: /.*/       # without this, every branch push also triggers this

Omitting branches: ignore: /.*/ when you have a tag filter is its own trap. CircleCI’s filter logic on workflows is OR-based by default β€” a job runs if the ref matches the branch filter OR the tag filter. So if you only specify tags: only: /^v.*/ and leave branches unset, every branch push still triggers the setup workflow (because unset branch filter defaults to “all branches”). You end up with duplicate pipeline runs on branch pushes: one normal, one that goes through your setup path. That burns minutes and causes genuinely confusing pipeline histories.

The pipeline.git.tag variable is available in your setup config, but you have to explicitly pass it through to the continuation step as a pipeline parameter β€” it won’t automatically survive into the generated config. Here’s a full setup job that handles tag triggers correctly and propagates the tag downstream:

version: 2.1

setup: true

orbs:
  continuation: circleci/[email protected]

jobs:
  generate-config:
    docker:
      - image: cimg/python:3.12
    steps:
      - checkout
      - run:
          name: Generate downstream config
          command: |
            python scripts/generate_config.py \
              --tag "<< pipeline.git.tag >>" \
              --output /tmp/generated_config.yml
      - continuation/continue:
          configuration_path: /tmp/generated_config.yml
          # Pass tag as a parameter so generated config can branch on it
          parameters: '{"deploy_tag": "<< pipeline.git.tag >>"}'

workflows:
  setup-workflow:
    jobs:
      - generate-config:
          filters:
            tags:
              only: /^v.*/
            branches:
              ignore: /.*/

Your generated config then needs to declare deploy_tag as a pipeline parameter at the top, otherwise the continuation call throws a parameter validation error that looks completely unrelated to tags:

# generated_config.yml (or your template)
version: 2.1

parameters:
  deploy_tag:
    type: string
    default: ""   # empty string = branch build, non-empty = tag build

workflows:
  deploy:
    when:
      not:
        equal: ["", << pipeline.parameters.deploy_tag >>]
    jobs:
      - deploy-production

One more gotcha: pipeline.git.tag evaluates to an empty string on branch pushes, not to null. So any when condition in your generated config checking for the tag needs to handle the empty string case explicitly, as shown above. If you check for truthiness instead of an empty string comparison, you can get undefined behavior depending on which YAML anchors or custom logic you’ve layered on top.

The ‘No Workflow’ Error: Five Actual Root Causes

The “no workflow” error is deliberately unhelpful β€” CircleCI just shows a pipeline with zero workflows attached and gives you nothing to go on. I’ve traced it to five distinct causes, and they’re not all obvious even after you’ve read the docs twice.

Cause 1 β€” Tag filter missing from the setup job

This one burns people the most because it feels like it should just work. Your .circleci/config.yml has a setup pipeline with a single job, and that job calls the continuation orb. But if you didn’t add a tag filter to the setup job itself, CircleCI drops the pipeline before it ever calls the continuation API. The setup workflow filters are evaluated first. Here’s what the broken version looks like versus the fixed version:

# BROKEN β€” tag push triggers nothing
workflows:
  setup:
    jobs:
      - setup-dynamic-config

# FIXED β€” tag filter must live on the setup job too
workflows:
  setup:
    jobs:
      - setup-dynamic-config:
          filters:
            tags:
              only: /^v.*/
            branches:
              ignore: /.*/

The mental model that helps: CircleCI evaluates the top-level config like any normal pipeline first. If no job in that file matches the trigger (a tag push in this case), the pipeline ends. The dynamic continuation never gets a chance to run.

Cause 2 β€” Invalid pipeline parameters schema returning a silent 400

The continuation API at https://circleci.com/api/v2/pipeline/continue returns HTTP 400 when your parameters payload doesn’t match the schema declared in your continued config. The UI just shows “no workflow” β€” it does not surface the 400 or the error body. You can catch this locally before you push by mimicking the API call with curl:

# Grab your continuation key from the setup job's environment
curl -X POST https://circleci.com/api/v2/pipeline/continue \
  -H "Circle-Token: $CIRCLECI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "continuation-key": "YOUR_KEY_HERE",
    "configuration": "'"$(cat .circleci/continue_config.yml)"'",
    "parameters": {
      "deploy_env": "production",
      "run_integration": true
    }
  }'

If you get back {"message":"invalid continuation key"} that’s expected (keys expire), but a 400 with a body like "parameter 'deploy_env' not found in schema" tells you exactly what’s wrong. The mismatch is almost always a typo between the parameter name in the parameters: block at the top of continue_config.yml and what the setup job passes via circleci/continuation orb parameters. They must be identical strings.

Cause 3 β€” Continued config loads but every workflow filters out the tag

This one is subtle because the continuation succeeds β€” you can confirm that with the API call above β€” but the continued pipeline also ends with no workflow. The reason is that continue_config.yml has workflows with branch or tag filters that don’t match a tag push. A common accident is copying a config that was originally branch-only:

# This workflow will never run on a tag push
workflows:
  deploy:
    jobs:
      - deploy-job:
          filters:
            branches:
              only: main   # tag pushes don't have a branch β€” they're excluded

Tag pushes in CircleCI are evaluated against tag filters, not branch filters. If a job only has a branches filter and no tags filter, it is excluded on tag pushes. Fix it by explicitly allowing the tag pattern:

filters:
  tags:
    only: /^v.*/
  branches:
    ignore: /.*/

Cause 4 β€” Continuation orb version with empty-string parameter bug

Versions of the circleci/continuation orb below 0.3.0 had a bug where passing an empty string as a parameter value caused the API call to be constructed with a malformed body. The pipeline would be silently rejected. You’d never see it fail β€” the setup job would exit 0. Check your orb version:

orbs:
  continuation: circleci/[email protected]  # anything below 0.3.0 is risky

The workaround if you’re stuck on an older version is to never pass empty strings β€” use a sentinel value like "none" or "false" and handle that in your workflow conditions. But honestly just bump the orb version. The changelog is sparse but 0.3.1 is stable and handles empty strings correctly.

Cause 5 β€” YAML syntax error in the continued config validated at continuation time

CircleCI validates .circleci/config.yml at push time, but continue_config.yml (or whatever file you’re passing to the continuation API) is validated only when the continuation call is made β€” which happens inside the setup job during the pipeline run. A YAML syntax error in that file will kill the pipeline with no workflow, and the error appears nowhere visible unless you’re looking at the setup job’s raw output very carefully.

Validate the file locally before every push. The circleci CLI handles this:

# CircleCI CLI v0.1.29000+
circleci config validate .circleci/continue_config.yml

If the CLI isn’t available in your environment, python3 -c "import yaml, sys; yaml.safe_load(open(sys.argv[1]))" .circleci/continue_config.yml catches structural YAML errors, though it won’t catch CircleCI-specific schema violations. The most common syntax culprit I’ve seen is multiline shell commands in run steps with inconsistent indentation β€” YAML is unforgiving there and the error message from the continuation API response body is usually clear if you actually read it via the curl method above.

Debugging Workflow: How to Actually Figure Out What’s Wrong

The “no workflow” error almost never tells you what’s actually broken. CircleCI drops the workflow silently when something goes wrong in the continuation phase, which means your debugging instinct to stare at the final pipeline output is completely wrong. You need to work backwards from the setup pipeline, not forward from the failed one.

Step 1 β€” Check the Setup Pipeline, Not the Continued Pipeline

Go to your CircleCI dashboard and filter pipelines by the trigger source. Your setup pipeline runs first β€” it’s the one executing the job that calls continuation/continue. The continued pipeline (the one with no workflows) is already dead by the time you’re looking at it. In the CircleCI UI, click into the setup pipeline, find the continuation job, and expand the continuation orb step output specifically. This is where API errors actually surface. I’ve seen teams spend hours looking at the wrong pipeline because the UI makes them look equivalent.

Step 2 β€” Validate Both Configs Locally

You need the CircleCI CLI installed (circleci update if you have it, or grab it from the official install page). Run validation on both files independently:

# Validate your setup config (the one that runs first)
circleci config validate .circleci/config.yml

# Validate the continued config (the one that gets injected)
circleci config validate .circleci/continue_config.yml

The thing that tripped me up: config validate will pass for continue_config.yml even if you have parameter declarations that don’t match what the setup job is passing. Local validation checks YAML structure and known keys, not runtime parameter compatibility. So a clean validation output doesn’t mean you’re clear β€” it just means the YAML isn’t malformed.

Step 3 β€” Replay the Continuation API Call with curl

This is the one step almost nobody does, and it’s the fastest way to get a real error message. Grab your CircleCI personal API token and the continuation key from your setup job’s environment (it’s exposed as CIRCLE_CONTINUATION_KEY during the setup job). Reconstruct the POST manually:

curl -X POST \
  https://circleci.com/api/v2/pipeline/continue \
  -H "Content-Type: application/json" \
  -H "Circle-Token: YOUR_PERSONAL_API_TOKEN" \
  -d '{
    "continuation-key": "YOUR_CONTINUATION_KEY",
    "configuration": "version: 2.1\nworkflows:\n  test:\n    jobs:\n      - hello\njobs:\n  hello:\n    docker:\n      - image: cimg/base:2024.01\n    steps:\n      - run: echo hello",
    "parameters": {
      "run_integration": false
    }
  }'

When this fails, the API actually returns a meaningful error body β€” something like {"message": "parameter 'run_integration' expects type boolean but received string"}. That’s infinitely more useful than the silent no-workflow state. You can’t replay this with an expired continuation key (they’re single-use and short-lived), but you can add a step that logs the key and immediately pauses so you can capture it during a debug run.

Step 4 β€” Add Debug Output to Your Setup Job

Before the continuation step fires, add explicit echo statements. This sounds obvious but most people skip it because they assume the orb handles everything correctly:

steps:
  - run:
      name: Debug β€” show parameters being passed
      command: |
        echo "run_integration: << pipeline.parameters.run_integration >>"
        echo "deploy_env: << pipeline.parameters.deploy_env >>"
        echo "Config file being continued: .circleci/continue_config.yml"
        ls -la .circleci/
  - continuation/continue:
      configuration_path: .circleci/continue_config.yml
      parameters: '{"run_integration": << pipeline.parameters.run_integration >>, "deploy_env": "<< pipeline.parameters.deploy_env >>"}'
  - run:
      name: Debug β€” continuation step exit code
      command: echo "Continuation orb exited successfully"
      when: on_success

The when: on_success step after the continuation call tells you whether the orb returned a non-zero exit. If you never see “Continuation orb exited successfully” in the logs, the orb itself threw an error β€” look at the orb step output directly above it for the API response body.

Step 5 β€” Check Parameter Type Mismatches (the Silent Killer)

This one burned me badly. If continue_config.yml declares a parameter as type: string and your setup config passes an integer, CircleCI doesn’t throw a validation error β€” it silently drops the entire workflow. Same thing happens with boolean vs string mismatches when your parameter gets interpolated into JSON without quotes:

# In continue_config.yml β€” this expects a boolean
parameters:
  run_integration:
    type: boolean
    default: false

# In config.yml setup job β€” this is WRONG, it passes the string "true"
parameters: '{"run_integration": "true"}'

# Correct β€” no quotes around a boolean value in JSON
parameters: '{"run_integration": true}'

The maddening part is that circleci config validate on the continue_config won’t catch this because it doesn’t know what parameters are being passed at invocation time. Audit your parameter declarations in continue_config.yml line by line against the JSON string you’re constructing in the setup job. Pay special attention to tag-triggered pipelines β€” if you’re passing the git tag as a parameter, it’s always a string, so make sure the receiving parameter is type: string, not type: integer, even if the tag looks like a version number.

The Fix: Working Config for Tag-Triggered Dynamic Pipelines

The “No Workflow” error in dynamic config tag pipelines almost always comes down to one of three things: the setup config’s tag filter not matching, the continued config’s workflow not having its own tag filter, or the parameter block being mismatched. I’ve burned hours on all three. Here’s the complete working setup.

The Setup Config: .circleci/config.yml

The setup config is the gatekeeper. If your tag filter isn’t on both the workflow and the job inside it, CircleCI silently skips everything and you get the dreaded blank pipeline. Yes, both. The filter has to be declared twice.

version: 2.1

setup: true

orbs:
  continuation: circleci/[email protected]

parameters:
  # Nothing here β€” this is the setup config.
  # Parameters live in continue_config.yml.

workflows:
  setup-workflow:
    jobs:
      - setup-dynamic-config:
          # Without this block on the workflow, tag pipelines are ignored entirely.
          filters:
            tags:
              only: /^v.*/
            branches:
              ignore: /.*/

jobs:
  setup-dynamic-config:
    docker:
      - image: cimg/base:2024.01
    steps:
      - checkout
      - run:
          name: Determine git tag and pass to continued config
          command: |
            # pipeline.git.tag is available as an env var in the setup job.
            # We serialize it into a JSON params file for the continuation orb.
            GIT_TAG="${CIRCLE_TAG:-}"
            echo "Detected tag: '$GIT_TAG'"
            cat \<<EOF > /tmp/pipeline-params.json
            {
              "git_tag": "$GIT_TAG"
            }
            EOF
      - continuation/continue:
          configuration_path: .circleci/continue_config.yml
          parameters: /tmp/pipeline-params.json

One thing that caught me off guard: CIRCLE_TAG is empty string on branch pushes, not undefined. So the :- fallback is defensive but harmless β€” what matters is that you always write the key to the JSON file, even with an empty value. If the key is missing, the continuation step will error on parameter validation before your pipeline even starts.

The Continue Config: .circleci/continue_config.yml

This is where most people get it wrong. The continued config needs its own tag filter on the deploy workflow. The setup config’s filters don’t carry over β€” CircleCI treats this as a fresh pipeline evaluation. If you skip the filter here, the deploy workflow runs on every branch push too, which is usually not what you want.

version: 2.1

parameters:
  git_tag:
    # Must be type: string. CircleCI doesn't support enum or union types here.
    # Default must be empty string, not "none" or null β€” those cause type errors.
    type: string
    default: ""

workflows:
  # Runs on branches, explicitly ignores tags.
  test:
    when:
      not:
        equal: [ "", << pipeline.parameters.git_tag >> ]
    # Actually, for branch-only: use filters, not `when`, to ignore tags.
    jobs:
      - run-tests:
          filters:
            tags:
              ignore: /.*/

  # Only runs when the setup job detected and passed a non-empty git_tag.
  deploy:
    when:
      not:
        equal: [ "", << pipeline.parameters.git_tag >> ]
    jobs:
      - run-tests:
          filters:
            tags:
              only: /.*/
      - deploy-to-production:
          requires:
            - run-tests
          filters:
            tags:
              only: /.*/

jobs:
  run-tests:
    docker:
      - image: cimg/node:20.11
    steps:
      - checkout
      - run: npm ci
      - run: npm test

  deploy-to-production:
    docker:
      - image: cimg/base:2024.01
    steps:
      - checkout
      - run:
          name: Deploy
          command: |
            echo "Deploying tag: << pipeline.parameters.git_tag >>"
            # Your actual deploy script here.
            ./scripts/deploy.sh << pipeline.parameters.git_tag >>

Making Tests AND Deploy Run on Tags

The trick with running tests before deploy in a continued config is the requires + filters combination. If you add requires: [run-tests] to your deploy job but forget to also put the tag filter on run-tests inside the deploy workflow, CircleCI will refuse to run the whole workflow. Both jobs in the same workflow need matching filters. This is not documented clearly anywhere I could find β€” I hit it by trial and error.

workflows:
  deploy:
    when:
      not:
        equal: [ "", << pipeline.parameters.git_tag >> ]
    jobs:
      # run-tests MUST have the tag filter here too,
      # even though it's not the final deployment step.
      - run-tests:
          filters:
            tags:
              only: /^v.*/

      - deploy-to-production:
          requires:
            - run-tests          # blocks deploy until tests pass
          filters:
            tags:
              only: /^v.*/      # must match the filter on run-tests exactly

If you want the test workflow to also run on tag pushes (for visibility in the pipeline UI), remove the tags: ignore: /.*/ filter from the test workflow and instead rely solely on the when: not equal condition to gate the deploy workflow. Just be aware this means you’ll see two test runs on a tag push β€” one from the test workflow, one from the deploy workflow. Most teams accept this trade-off because the alternative (sharing jobs across workflows) isn’t supported in CircleCI’s model. The deploy workflow’s run-tests job is the canonical gate; the test workflow’s run is just noise.

Validating Before You Push

Don’t push to test this loop β€” the round-trip feedback is painful. Use the CLI locally first:

# Validate both configs independently
circleci config validate .circleci/config.yml
circleci config validate .circleci/continue_config.yml

# Pack and process the setup config to catch orb resolution errors
circleci config process .circleci/config.yml

# Simulate what the continuation step sees by passing params manually
circleci local execute --job setup-dynamic-config

The config process command will expand orbs inline and show you the resolved YAML β€” that’s where you’ll see if the continuation orb version is resolving correctly and whether your parameter JSON structure matches what the orb expects. The continuation orb at 1.0.0 expects a flat JSON object; nested objects will silently drop keys in my experience with it.

Monorepo Add-On: When You’re Also Doing Path Filtering on Tags

The worst version of the “no workflow” error I’ve hit wasn’t from a single misconfigured tag filter β€” it was from two systems failing silently at the same time. Path-based continuation logic generates a continue_config.yml dynamically, then hands off to the continuation orb. Tag filters sit in your setup config. When a tag push happens, both need to cooperate: the setup workflow has to match the tag, the path-filtering logic has to not bail early, and the generated config has to have its own workflow-level tag filters. Any one of those three failing produces the same result β€” CircleCI reports the pipeline as triggered but no workflows run. You get nothing in the UI, no error, just silence.

The compounding problem is that path-filtering orbs evaluate changed files against a base branch. On a tag push, there’s no diff the orb can compute in the obvious way β€” tags don’t have a “changed since last tag” diff baked into the CircleCI environment automatically. If your setup config calls the path-filtering orb directly on a tag trigger without explicitly setting base-revision, the orb may evaluate zero changed paths, generate a continue_config.yml with no pipeline parameters set to true, and your continuation config’s workflows all have conditions like when: << pipeline.parameters.run-service-a >> β€” which are all false. Silent death.

Here’s the pattern I use to make the path-filtering orb and tag triggers coexist without fighting each other. The setup config has two workflows: one for branch pushes that uses the orb normally, and a separate one for tags that skips the orb entirely and calls a custom continuation job:

# .circleci/config.yml (setup config)
version: 2.1
setup: true

orbs:
  path-filtering: circleci/[email protected]
  continuation: circleci/[email protected]

parameters:
  # populated by tag regex match in the executor
  service-name:
    type: string
    default: ""

workflows:
  # branch pushes: use the orb, let it do path diffing normally
  path-filter-on-branch:
    when:
      not:
        matches:
          pattern: "^v.+"
          value: << pipeline.git.tag >>
    jobs:
      - path-filtering/filter:
          base-revision: main
          config-path: .circleci/continue_config.yml
          mapping: |
            services/service-a/.* run-service-a true
            services/service-b/.* run-service-b true
            services/service-c/.* run-service-c true

  # tag pushes: skip path filtering, derive service from tag name
  tag-deploy:
    when:
      matches:
        pattern: "^v.+"
        value: << pipeline.git.tag >>
    jobs:
      - generate-and-continue:
          filters:
            branches:
              ignore: /.*/
            tags:
              only: /^v.+/

The generate-and-continue job is where the real work happens. I extract the service name from the tag (my tags look like v1.4.2-service-a), generate a minimal continue_config.yml that only activates that service’s deploy workflow, validate the YAML before passing it to the continuation orb, and then continue. Validating before continuing is the part most people skip β€” if your script generates malformed YAML, the continuation API returns a cryptic 400 and you’re back to debugging blind:

# scripts/generate-continue-config.sh
#!/bin/bash
set -euo pipefail

TAG="${CIRCLE_TAG:-}"
SERVICE=$(echo "$TAG" | grep -oP '(?<=\d-)[a-z-]+$' || true)

if [[ -z "$SERVICE" ]]; then
  echo "ERROR: Could not extract service name from tag: $TAG"
  exit 1
fi

PARAM_NAME="run-${SERVICE}"

cat > /tmp/continue_config.yml <>
    jobs:
      - deploy:
          filters:
            tags:
              only: /^v.+/
            branches:
              ignore: /.*/

jobs:
  deploy:
    docker:
      - image: cimg/base:2024.01
    steps:
      - checkout
      - run: echo "Deploying ${SERVICE} from tag ${TAG}"
EOF

# validate YAML before handing off β€” catches template bugs immediately
python3 -c "import yaml, sys; yaml.safe_load(open('/tmp/continue_config.yml'))" \
  && echo "YAML valid" \
  || { echo "YAML validation failed"; cat /tmp/continue_config.yml; exit 1; }

# now pass pipeline parameters so the workflow condition is true
circleci-agent step halt 2>/dev/null || true  # not needed here, just defensive

# the continuation orb executor handles the actual API call,
# but if you're doing it manually:
curl --request POST \
  --url "https://circleci.com/api/v2/pipeline/continue" \
  --header "Circle-Token: ${CIRCLE_CONTINUATION_KEY}" \
  --header "Content-Type: application/json" \
  --data "{
    \"continuation-key\": \"${CIRCLE_CONTINUATION_KEY}\",
    \"configuration\": $(cat /tmp/continue_config.yml | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read()))'),
    \"parameters\": {\"${PARAM_NAME}\": true}
  }"

The parameters field in the continuation API call is what most people miss. Your generated config can have a workflow behind a when: << pipeline.parameters.run-service-a >> condition, but if you don’t pass {"run-service-a": true} in the continuation request body, that parameter defaults to false and the workflow never runs. The YAML is valid, the pipeline continues, and zero workflows appear. This is the specific gotcha that made me add YAML validation β€” once I confirmed the config structure was correct, the bug was obviously the missing parameters object in the API call.

For the concrete monorepo scenario: you’ve got services A, B, and C under services/. You cut a release tag v2.0.1-service-b specifically for service B. The setup workflow matches the tag pattern, skips the path-filtering orb entirely, runs generate-and-continue.sh, which extracts service-b, generates a config that only defines the run-service-b parameter and the deploy-service-b workflow, validates the YAML passes yaml.safe_load, then calls the continuation API with {"run-service-b": true}. Service A and C don’t appear in the generated config at all, so there’s no risk of them accidentally triggering. The continued pipeline shows exactly one workflow, one job, no ambiguity.

Things the Docs Don’t Tell You (But Should)

The first thing that’ll bite you: the setup pipeline itself is a real pipeline execution. Every time you push a tag and your setup pipeline runs β€” even if it calls circleci-agent step halt or produces zero continuation β€” CircleCI bills you for those compute minutes. I burned through a surprising chunk of credits in one afternoon just iterating on my setup.yml logic, not realizing each failed attempt was clocking up time on the setup executor. Switch to the smallest executor you can for setup pipelines. resource_class: small on a Linux machine costs a fraction of medium, and your setup pipeline is usually just running a few shell conditionals and a curl call anyway.

# setup.yml β€” keep this ruthlessly small
setup: true
jobs:
  trigger-dynamic:
    docker:
      - image: cimg/base:stable
    resource_class: small  # don't burn credits on setup overhead
    steps:
      - checkout
      - run:
          name: Decide which pipeline to continue with
          command: |
            TAG="${CIRCLE_TAG:-}"
            if [[ -z "$TAG" ]]; then
              echo "Not a tag push, halting"
              circleci-agent step halt
            fi
      - continuation/continue:
          configuration_path: .circleci/deploy.yml
          parameters: '{"deploy_tag": "'"$CIRCLE_TAG"'"}'

The context isolation thing is genuinely confusing and the docs bury it. When the Continuation API kicks off your continued pipeline, it’s a brand new pipeline execution β€” not a continuation of the original push event. That fresh pipeline has no memory of the git ref that triggered the setup. pipeline.git.tag inside your deploy.yml will be empty unless you explicitly pass it as a parameter. This is the root cause of most “my deploy job runs but then can’t find the tag” bugs. The fix is always the same: extract the tag in the setup pipeline where CIRCLE_TAG is populated, and pass it forward as a string parameter.

# In your setup pipeline, pass the tag explicitly:
- continuation/continue:
    configuration_path: .circleci/deploy.yml
    parameters: |
      {
        "deploy_tag": "<< pipeline.git.tag >>",
        "run_deploy": true
      }

# In deploy.yml, declare it as a parameter β€” not as a filter:
parameters:
  deploy_tag:
    type: string
    default: ""
  run_deploy:
    type: boolean
    default: false

The UI symptom that wastes the most debugging time: CircleCI shows “No workflow” for two completely different failure modes. If your tag filter pattern doesn’t match, you get “No workflow.” If the Continuation API returns a 400 because your JSON payload is malformed, you also get “No workflow.” There’s no visual distinction. The way I tell them apart β€” click into the setup pipeline, find the continuation step, and look at the raw step output. An API error will show something like Error: 400 Bad Request in the agent output. A filter-exclusion failure produces no error at all; the setup pipeline just exits cleanly with no continuation call. If the setup pipeline shows green and no continuation step ran, it’s a logic problem in your setup script. If continuation ran but the continued pipeline shows “No workflow,” then you’ve got a workflow-level filter issue inside the continued config itself.

The when clause beats filters for tag-gated jobs in continued configs almost every time. The reason is subtle: filters.branches and filters.tags in a continued pipeline are evaluated against the continued pipeline’s own trigger context β€” which, as mentioned above, carries no original tag unless you’ve reconstructed it. So a filter like tags: only: /^v.*/ inside deploy.yml will silently exclude the job because from the continued pipeline’s perspective there’s no tag in context. Contrast with when: << pipeline.parameters.run_deploy >> β€” that evaluates against the parameters you explicitly passed in, which you control completely. Here’s the pattern that actually works reliably:

# deploy.yml
parameters:
  run_deploy:
    type: boolean
    default: false
  deploy_tag:
    type: string
    default: ""

workflows:
  deploy:
    when: << pipeline.parameters.run_deploy >>
    jobs:
      - build-and-push:
          context: production
      - deploy:
          requires:
            - build-and-push

jobs:
  deploy:
    docker:
      - image: cimg/base:stable
    steps:
      - run: echo "Deploying tag << pipeline.parameters.deploy_tag >>"
      # use the parameter, not CIRCLE_TAG, which may be empty here

One more gotcha that’s not documented anywhere clearly: if you use the circleci/continuation orb, the orb version matters. Orb [email protected] has subtly different parameter-passing behavior than [email protected]. I’ve seen setups where upgrading from 0.4.0 to 1.0.0 changed how empty string parameters were handled, which caused previously-working boolean flags to evaluate differently. Pin your orb version in setup.yml and don’t let Renovate auto-bump it without a test push on a throwaway tag first.

FAQ

You didn’t include the FAQ points to cover, but I’ll build the most common real-world questions I’ve seen developers hit when debugging the “no workflow” error with CircleCI Dynamic Config and tag pipelines.

Why does my tag push trigger a pipeline but no workflows run?

This is the classic symptom. CircleCI shows a pipeline was created, but the workflows list is empty or shows “no workflows.” Almost always this means your setup workflow ran, the continuation step fired, but the continued config had no workflow with a filter that matched your tag β€” or you forgot to add the when clause entirely. The pipeline exists because the setup phase succeeded. The silence after that is your continuation config rejecting all workflows silently.

Do I need tag filters in both the setup config and the continuation config?

Yes, and this trips up nearly everyone the first time. If your .circleci/config.yml (the setup config) has a setup: true key and a single setup workflow, that workflow runs unconditionally β€” tag filters there don’t gate anything downstream. The tag filter that actually controls whether your build/deploy workflow runs must live inside the continuation config, on each job’s filters block. If you omit it in the continuation config, the workflow won’t trigger on tags, full stop.

# continuation config β€” this is the file you pass to the continuation orb
workflows:
  deploy-on-tag:
    jobs:
      - build:
          filters:
            branches:
              ignore: /.*/         # ignore all branches
            tags:
              only: /^v[0-9]+.*/   # only semantic version tags
      - deploy:
          requires:
            - build
          filters:
            branches:
              ignore: /.*/
            tags:
              only: /^v[0-9]+.*/

Every job in the workflow needs the filter. If deploy has the filter but build doesn’t, CircleCI will skip the whole workflow on a tag push. That’s a documented behavior that feels like a bug when you first hit it.

I’m using the continuation orb β€” what version should I be on?

Use circleci/[email protected] or later. Earlier 0.x versions had edge cases around parameter passing that caused silent failures when the generated config was valid YAML but had empty pipeline parameters. Check your orb version with:

cat .circleci/config.yml | grep "continuation@"

If you’re on 0.3.x, bump it. The diff between 0.3 and 1.0 is mostly in how it handles the config validation step before posting to the API β€” newer versions give you an actual error instead of a silent no-op.

How do I actually debug which config is being sent to the continuation API?

Add a step before continuation/continue that prints the generated config to stdout. Sounds obvious, but most people skip it and spend hours guessing.

steps:
  - run:
      name: Show continuation config
      command: cat /tmp/generated-config.yml   # or wherever you write it
  - continuation/continue:
      configuration_path: /tmp/generated-config.yml

Open the pipeline in the CircleCI UI, click into the setup workflow’s “Show continuation config” step, and read the actual YAML that got submitted. I’ve found misconfigured anchors, wrong indentation, and outright missing workflow blocks this way β€” all things that looked fine in my editor but got mangled by whatever script was generating the file.

Can pipeline parameters from a tag push reach the continuation config?

Yes, but you have to explicitly forward them. When you call continuation/continue, pass a parameters argument with a JSON string of whatever values you want available downstream. The tag name itself isn’t automatically forwarded β€” you have to capture it from the environment and inject it:

steps:
  - run:
      name: Set continuation parameters
      command: |
        echo "{\"deploy_tag\": \"$CIRCLE_TAG\"}" > /tmp/params.json
  - continuation/continue:
      configuration_path: /tmp/generated-config.yml
      parameters: /tmp/params.json

$CIRCLE_TAG is populated by CircleCI when the pipeline was triggered by a tag push. It’ll be empty on branch builds, so if your setup workflow logic depends on it, test for it explicitly rather than assuming it’s always set.

My continuation config validates locally with circleci config validate but still produces no workflows β€” why?

The CLI validator checks syntax and schema. It does not simulate filter evaluation against a specific trigger type. A config with only tag-filtered workflows will pass circleci config validate perfectly and then produce zero workflows when triggered by a branch push β€” or vice versa. To actually test filter behavior, use the CircleCI API to trigger a pipeline manually with a fake tag parameter and watch what happens, or use the pipeline simulation feature in the CircleCI web UI (Project Settings β†’ Triggers). There’s no local tool that fully replicates the filter resolution logic as of mid-2025.


Disclaimer: This article is for informational purposes only. The views and opinions expressed are those of the author(s) and do not necessarily reflect the official policy or position of Sonic Rocket or its affiliates. Always consult with a certified professional before making any financial or technical decisions based on this content.


Eric Woo

Written by Eric Woo

Lead AI Engineer & SaaS Strategist

Eric is a seasoned software architect specializing in LLM orchestration and autonomous agent systems. With over 15 years in Silicon Valley, he now focuses on scaling AI-first applications.

Leave a Comment