Skip to content

End conditions and pre-conditions ​

Script gates make workflows react to project state instead of trusting the model to declare success. You can write them in the workflow file or set them from the Validation tab of the Workflow properties panel.

End condition ​

An end condition runs after a prompt step completes.

yaml
endCondition:
  script: npm test
  timeoutSec: 60
  maxRetries: 2
  retryPrompt: |
    Validation failed.
    stdout:
    $endCondition:stdout

    stderr:
    $endCondition:stderr

A failed validation is re-prompted while retry budget remains. A retryPrompt alone does not enable retries: set maxRetries above 0 on the condition or in defaults.endCondition.maxRetries, or use onFail: retry. With onFail: retry, no template is required: ctrlyoke sends a fallback prompt containing the expected and actual values and script output. The implicit retry limit is one when onFail: retry is set and maxRetries is omitted. When the budget runs out the step fails and the workflow stops. onFail: continue skips retries and advances immediately.

onPass: retry works in the other direction: it re-prompts when a passing validation means the step should keep working. Pass retries use passRetryPrompt, then the default pass-retry prompt from settings, and finally the step's own prompt when no template is configured. They reuse the same maxRetries budget as failure retries and the same session by default.

maxSteps is a separate run-wide cap on entries in the workflow's steps array. It counts queued steps regardless of whether the workflow author, an agent, or the dashboard added them, but it does not count finalStep iterations or end-condition retries. A workflow can set maxSteps: -1 to override a numeric global queue.maxSteps cap. See Dynamic queue for the cap and append behavior.

When a pass-retry budget is exhausted while validation still passes, the step succeeds and the workflow advances. If a re-evaluation inside the pass-retry loop fails, the normal onFail action handles that result.

For example, this keeps asking for a regression test until the test actually fails, then advances to the next step:

yaml
steps:
  - prompt: Write a regression test for issue #123. Do not fix the bug.
    endCondition:
      script: npm test
      onPass: retry
      onFail: continue
      maxRetries: 3
      passRetryPrompt: |
        npm test still passes, so the new test does not reproduce the bug.
        Strengthen the test until it fails for the right reason.

This onPass: retry plus onFail: continue combination is the regression-test recipe: keep working while the condition passes, and advance once it fails.

Important fields ​

FieldMeaning
scriptCommand or script path to execute (required)
envExtra environment variables for the script. Secrets only as ${secret:NAME} values
timeoutSecTimeout in seconds. Defaults to the execution.scriptTimeoutSec setting
shellShell for inline command text, e.g. pwsh, bash, cmd. Ignored for script files
compareSourceWhich output to compare: exitCode (default), stdout, stderr
operatorHow to compare: eq (default), ne, contains, notContains, gte, gt, lte, lt, matches, isEmpty, isNotEmpty
expectedValue to compare against. Omit it to pass on exit code 0
ignoreCaseCase-insensitive text compare (string operators only)
maxRetriesShared retry limit across pass and failure retries. 0 = none, -1 = unlimited, max 100. Unset: 1 when onPass/onFail is retry, else the defaults.endCondition.maxRetries setting (0)
retryPromptOptional template sent after validation failure; onFail: retry uses a fallback prompt when no template is configured
passRetryPromptTemplate sent after validation passes with onPass: retry
resumeSessionOnRetryWhether retries stay in the same conversation (default true)
onPasscontinue (default), retry, or stopWorkflow
onFailcontinue, retry, or stopWorkflow (default)

endCondition and preCondition can also be set once at the workflow level (or in the defaults settings) and are inherited by every prompt step. Set either to null on a step to switch an inherited condition off.

Comparing output ​

A condition with no compareSource/operator/expected passes when the script exits 0. That covers most checkpoints — npm test, npm run lint, tsc — and is why the examples above carry no comparison fields at all. There is no boolean form: true and false are not accepted as exit-code values, because exit 0 means "no error", not "true".

To compare output instead, state all three parts. They read as a sentence, and the dashboard shows the same sentence under the controls:

yaml
endCondition:
  script: npm test
  compareSource: stdout # which output: exitCode (default), stdout, stderr
  operator: contains # how to compare: eq (default), contains, gte, matches, …
  expected: "0 failing" # what to compare against
OperatorCompares
eq (default) / neThe whole trimmed value, literally. No characters are special.
contains / notContainsWhether the value contains expected as a substring.
gte / gt / lte / ltNumbers. Both sides must parse as a number, or the condition fails.
matchesexpected as a regular expression, unanchored — add ^/$ for a full match.
isEmpty / isNotEmptyWhether the value is empty. Takes no expected.

Three things worth knowing:

  • The operator is never inferred. The comparison you write is the comparison that runs, whatever the script happens to print.
  • Text comparisons ignore the exit code, and exitCode comparisons ignore the text. If you need both, that is two conditions, not one.
  • Text is trimmed and case-sensitive. Add ignoreCase: true for a case-insensitive eq/ne/contains/notContains.
  • Script output is sanitized before comparison. ANSI control sequences are removed and CRLF line endings become LF, consistently for headless and interactive script execution. The sanitized stdout/stderr are also used by $endCondition:* and $preCondition:* output references.

Operators are word tokens (gte, not >=) because symbols are not usable as YAML values — operator: >= is a parse error, and operator: != parses as a YAML tag and silently becomes an empty string. The dashboard still shows you >= and !=.

Pre-condition ​

A pre-condition runs before a prompt step and decides whether to run the prompt, skip it, or stop the workflow. It uses the same script and comparison fields as an end condition (no retry fields).

yaml
preCondition:
  script: npm run lint
  onFail: stopWorkflow
FieldValues
onPasscontinue (default, run the prompt), skipPrompt, stopWorkflow
onFailcontinue, skipPrompt (default), stopWorkflow

onFail: continue makes the pre-condition advisory: the prompt runs anyway, and its output is still available through $preCondition:* references.

How scripts run ​

script is either a path to a script file or an inline command.

A path is looked up relative to the execution directory (the worktree, when the run uses one), then the workspace root, then .ctrlyoke/, then ~/.ctrlyoke/scripts/. A resolved file must stay inside the workspace or ~/.ctrlyoke/scripts/. The interpreter follows the file extension:

ExtensionRuns with
.pypython3, falling back to python
.js, .mjs, .cjsnode
.tsnode with a local tsx or ts-node
.shsh (bash on Windows)
.ps1PowerShell (-ExecutionPolicy Bypass -File)
.bat, .cmdcmd /c (Windows only)

Files without one of these extensions run directly when executable, or through the interpreter named in their shebang line.

Anything that does not resolve to a file runs as a shell command — pin that shell with shell, otherwise the terminal.shellExecutable setting or the platform default is used. Scripts run in the execution directory, and workflow variables are exported to them as CTRLYOKE_VAR_<KEY> environment variables.

A script that cannot be run at all — for example an unsupported file type or a spawn failure — is an internal error, not a failed validation. It is handled by the step's errorBehavior policy (stopWorkflow, retry, or continue) rather than by onFail.

Condition output references ​

Retry templates can use $endCondition:* references:

  • $endCondition:stdout
  • $endCondition:stderr
  • $endCondition:exitCode
  • $endCondition:actual
  • $endCondition:expected
  • $endCondition:compareSource

$preCondition:* references use the same six fields and work in both the step prompt and its retry prompt. $endCondition:* references are only available in retry prompts because the end condition runs after the prompt.

Source-available under the ctrlyoke Commercial License.