Trigger Policy
Contract
Iron law: merge and release MUST appear in every trigger's policy.forbidden_actions — triggers cannot grant Tier 3 autonomy regardless of any other configuration. Invoked in validate mode (read-only) by /flow:trigger create|enable|validate and by /flow:watch step 4 before a trigger YAML is written or enabled, and in enforce mode by /flow:run step 3 and /flow:trigger run before the target command is dispatched. Returns the JSON report below with exit 0 (pass or soft warning), 1 (policy violation), or 2 (schema invalid); the caller aborts on non-zero. Permitted skips: none — every step runs; the only non-blocking outcome is concurrency_warning.
Inputs
The invoking command MUST pass:
- Trigger YAML path — a template under
plugins/flow/triggers/templates/or a project-local.flow/triggers/<id>.trigger.yaml. - Mode —
validate | enforce.
Output
{
"trigger_id": "pr-123-watch",
"schema_valid": true,
"tier3_violations": [],
"recursion_violations": [],
"missing_required_forbidden": [],
"cross_reference_violations": [],
"concurrency_violations": [],
"overall": "pass"
}
Steps
- Schema validation:
python3 -m jsonschema -i "${TRIGGER_YAML}" "plugins/flow/schemas/v1/trigger.schema.json". Failure →overall: schema_invalid(exit 2). - Tier 3 absolute deny:
policy.forbidden_actionsmust contain bothmergeANDrelease. Missing either →tier3_violations.append({"action": "merge_or_release", "reason": "must be forbidden"}). Hard fail. - Recursion policy:
recursion_policy.triggered_runs_may_create_triggers,triggered_runs_may_modify_triggers, andtriggered_runs_may_enable_triggersmust befalseor unset (default false). Anytrue→recursion_violations; enabling it requires explicit Tier 3 authorization via AskUserQuestion at/flow:trigger createtime. - Allowed types:
trigger.typemust be inflow.triggers.allowedTypes(cascade-resolved; default[manual, hook, loop_prompt]).github_actions | local_cron | local_daemonare schema-valid but disabled in v3.0 — surface astier3_violationsunless the project's setting permits them. - Active-trigger count: count
.flow/triggers/*.trigger.yamlwithmetadata.enabled: trueand lifecycle != disabled. If count >=flow.triggers.maxActiveTriggers(cascade-resolved; default 5), refuse to enable a new trigger — the user must/flow:trigger disableone first. - Concurrency sanity:
concurrency.policy: cancel_previouson a trigger whose target invokes a Tier 2 action (push, commit) →concurrency_violationswarning; cancel_previous + Tier 2 can produce partial commits. Soft warning only — never a hard fail. - Target workflow cross-reference: when
target.workflowis set, the workflow must exist atplugins/flow/workflows/${WF}.workflow.yamlor.flow/workflows/${WF}.workflow.yaml([ -f "$PLUGIN_PATH" ] || [ -f "$LOCAL_PATH" ]). Missing →cross_reference_violations.append({"type": "missing_target_workflow", "name": target_workflow, "checked_paths": [PLUGIN_PATH, LOCAL_PATH]}). Hard fail (exit 1) — a trigger pointing at a non-existent workflow is broken by construction; this catches typos (addressvsaddress-pr) at creation rather than when/flow:run trigger <id>dispatches. Whentarget.workflowis absent (target.commandonly), this step is a no-op. - Overall verdict:
| Condition | overall |
|---|---|
| schema fails | schema_invalid (exit 2) |
tier3_violations non-empty |
tier3_violation (exit 1; HARD FAIL) |
recursion_violations non-empty |
recursion_violation (exit 1) |
cross_reference_violations non-empty |
cross_reference_failed (exit 1; HARD FAIL — missing target workflow) |
concurrency_violations non-empty |
concurrency_warning (exit 0 — soft warning) |
| else | pass (exit 0) |
Reuse map
plugins/flow/schemas/v1/trigger.schema.json— schema validated against.plugins/flow/triggers/templates/— plugin-shipped templates.plugins/flow/commands/trigger.md,watch.md,run.md— invoking commands.plugins/flow/references/flow-triggers.md— user-facing trigger documentation.