BLOG/DEVELOPER DAILY
THEME
DEVELOPER DAILY

Interactive prompts that auto-skip in CI

A runbook that asks 'deploy to prod?' is great for a human and a deadlock in CI. func: prompt asks when there's a terminal and quietly uses defaults when there isn't — one workflow for both.

Jun 26, 2026 OrchStep Team 6 minROLE: AnySCALE: Any

There are two kinds of deploy script and they hate each other. The interactive one asks "which environment?" and "are you sure?", which is exactly right when a human is driving. The CI one can't ask anything — a prompt in a pipeline is a deadlock — so it takes everything as flags and environment variables. Most teams maintain both, and they drift.

func: prompt collapses them into one. It asks when there's a terminal, and when there isn't — CI runner, piped input, an agent — it silently uses the default or a --var override. Same YAML, both audiences.

One workflow, two audiences

orchstep.yml
name: release
defaults:
  app: "myapp"
tasks:
  ship:
    steps:
      - name: target
        func: prompt
        args:
          message: "Target environment"
          type: select
          options: [dev, staging, production]
          default: staging
      - name: confirm
        func: prompt
        args:
          message: "Deploy {{ vars.app }} to {{ steps.target.value }}?"
          type: confirm
          default: false
      - name: run_it
        if: '{{ eq steps.confirm.value "true" }}'
        func: shell
        do: 'echo "deploying {{ vars.app }} to {{ steps.target.value }}"'
      - name: skipped
        if: '{{ eq steps.confirm.value "false" }}'
        func: shell
        do: 'echo "skipped deploy of {{ vars.app }}, target was {{ steps.target.value }}"'

At a terminal, this is a runbook: a single-key environment picker, then a yes/no confirm that defaults to no. A prompt's answer lands in result.value, which the conditional steps read.

Three ways CI skips a prompt

A prompt auto-skips — using its default — whenever any of these is true:

  1. ORCHSTEP_NON_INTERACTIVE=true is set
  2. stdin is not a terminal (piped input, CI runners)
  3. a --var matching the step name supplies the value

So in CI, with the safe defaults, the destructive branch simply doesn't fire:

$ ORCHSTEP_NON_INTERACTIVE=true orchstep run ship
skipped deploy of myapp, target was staging
Result: success

confirm defaulted to false, target defaulted to staging, and the deploy step was skipped — no hang, no accidental prod push. That's the safety property: the defaults decide what happens in automation, so you choose them deliberately.

Override exactly the answers you want

To actually deploy from CI, supply the answers by step name. A --var matching a prompt's step name skips that prompt and uses your value:

$ ORCHSTEP_NON_INTERACTIVE=true orchstep run ship --var confirm=true --var target=production
deploying myapp to production
Result: success

Same workflow, no edits — the human path and the CI path are one file with different inputs.

The prompt types

TypeUse for
textfree input (default if type omitted)
passwordmasked secrets
selectone choice from options
confirmyes/no, returns "true" / "false"
multiselectseveral choices from options

A select with a default becomes a no-op that returns the default in CI; a confirm defaulting to false becomes a guard that stays shut unless a human says yes or a --var opts in.

What you gained

Two scriptsOne func: prompt workflow
interactive + CI versions driftone file, both paths
prompt in CI = deadlockauto-skips to defaults
"are you sure?" only locallyconfirm guards every run
CI inputs as ad-hoc flags--var <step>=value by name
no safe default = risky defaultdefault is an explicit choice

A note on conditionals

confirm returns the string "true" or "false", so compare against the quoted strings: if: '{{ eq steps.confirm.value "true" }}'. (A common slip is comparing against an unquoted boolean.) And reference workflow defaults as {{ vars.app }} — the vars namespace is what renders in do: commands and prompt messages alike.

Where this is not the answer

If a value is always supplied by automation and never typed, skip the prompt and make it a plain variable with a --var — a prompt that only ever returns its default is just indirection. And a password prompt collects a secret for the current run; it doesn't store one. For credentials that live across runs, use the secrets layer so they're masked and kept out of history.

Where to go next

Take your scariest runbook step — the one with a comment that says "double-check before running" — and make it a confirm prompt that defaults to false. It's safe in CI the same day.

#PROMPT#CI-CD#RUNBOOKS#AUTOMATION
Try it in two minutes — one binary, no signup.
curl -fsSL https://orchstep.dev/install.sh | sh

RELATED — DEVELOPER DAILY