Skip to content

feat: support conditional edges in composed workflows - #347

Open
eshulman2 wants to merge 3 commits into
mainfrom
feature/conditional-workflow-edges
Open

eshulman2 wants to merge 3 commits into
mainfrom
feature/conditional-workflow-edges

Conversation

@eshulman2

Copy link
Copy Markdown
Collaborator

Summary

Adds typed, declarative cases and otherwise transitions so composed workflows can choose edges from Forge-owned facts. The compiler validates and records the selected case; composition, rendering, diffs, and the workflow authoring docs support the new form. Static routers now have explicit reviewed outcome contracts, while protected approval and review gates retain trusted routing.

Validation

  • uv run pytest -q tests/unit/workflow — 1,087 passed
  • uv run ruff check src/forge/workflow/declarative/{models,predicates,compiler,composition,manifest,cli,router_contracts}.py tests/unit/workflow/test_conditional_edges.py
  • git diff --check

Stacked on #346 (feature/reusable-workflows).

@eshulman2

Copy link
Copy Markdown
Collaborator Author

Demo: authored conditional edges

From this branch, create a small local workflow:

cat > /tmp/forge-conditional-demo.yaml <<'YAML'
apiVersion: forge/v1
kind: Workflow
metadata:
  name: conditional-demo
  revision: 1
spec:
  state: feature
  entry: generate_prd
  steps:
    generate_prd:
      cases:
        - when: {fact: workflow.has_error, op: equals, value: true}
          next: escalate_blocked
        - when: {fact: workflow.paused, op: equals, value: false}
          next: generate_spec
      otherwise: __end__
    escalate_blocked:
      next: __end__
    generate_spec:
      next: __end__
YAML

uv run forge workflow catalog feature   # See the approved conditional facts
uv run forge workflow validate /tmp/forge-conditional-demo.yaml
uv run forge workflow render /tmp/forge-conditional-demo.yaml
uv run pytest -q tests/unit/workflow/test_conditional_edges.py

render shows the two labeled conditional edges and the otherwise edge. The focused tests exercise first-match selection, blocked runs, subworkflow exit binding, change detection, and a publishable feature workflow whose CI router is replaced with authored conditions. To see the three decisions directly:

uv run python - <<'PY'
from forge.workflow.declarative.compiler import DeclarativeWorkflowCompiler
from forge.workflow.declarative.loader import load_workflow_file

definition = load_workflow_file('/tmp/forge-conditional-demo.yaml')
step = definition.spec.steps['generate_prd']
route = DeclarativeWorkflowCompiler(definition)._conditional_route(step.cases, step.otherwise)
for label, state in [
    ('generation failed', {'last_error': 'failed', 'is_paused': False}),
    ('ready to continue', {'is_paused': False}),
    ('waiting for input', {'is_paused': True}),
]:
    print(f'{label}: {route(state)}')
PY

Expected targets: escalate_blocked, generate_spec, and __end__. This compact example is for local validation; publication still requires the feature profile's mandatory gates.

outcome: replacements.get(target, target)
for outcome, target in step.branches.items()
},
"cases": tuple(

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 — include conditional terminals in normal completion detection. These new case/otherwise targets can be rewritten to returnTo, but the included-Workflow path at line 132 selects terminals only when step.next == "__end__". A child whose conditional edge is its only normal completion is rejected with has no normal completion; if another fixed terminal exists, the conditional __end__ remains unchanged and ends the parent early. Include conditional terminal edges when selecting steps to rewrite.

def validate_transition(self) -> WorkflowStep:
if bool(self.next) == bool(self.route):
raise ValueError("exactly one of 'next' or 'route' is required")
if sum((bool(self.next), bool(self.route), bool(self.cases))) != 1:

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 — reject branches on a conditional step. This validator requires one of next, route, or cases, but the later branches check only rejects them with next. cases plus branches is accepted; the compiler follows cases and silently ignores the authored branches, even when a branch names an undeclared step. Please require branches only when route is set.

Base automatically changed from feature/reusable-workflows to main October 1, 2026 08:12

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant