Skip to content

feat: Conditional Execution - Option 1 - #2651

Merged
camielvs merged 1 commit into
masterfrom
08-20-feat_conditional_execution_-_option_1
Aug 24, 2026
Merged

feat: Conditional Execution - Option 1#2651
camielvs merged 1 commit into
masterfrom
08-20-feat_conditional_execution_-_option_1

Conversation

@camielvs

@camielvs camielvs commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Description

Implements Option 1 — Derived. Two alternatives were prototyped alongside it (#2652, #2654); this is the one we settled on. Also a general pass over the UI, UX and frontend architecture of the feature.

The question these options were answering

TaskSpec.isEnabled decides whether a task runs. It either holds a literal ("false") or points at an upstream value — a graph input, or another task's output.

The editor needs to know one thing the spec doesn't record: whether the user wants the conditional UI on this task at all — the extra handle on the node, the condition control in the Config panel. A task with no isEnabled looks exactly like a task that was never meant to be conditional, so where does that piece of state live?

The three answers

  • Option 2 — Annotation (feat: Conditional Execution - Option 2 #2652): keep an isConditional annotation on the task, fully decoupled from isEnabled. The annotation controls the UI; isEnabled controls the backend.
  • Option 4 — Always-on (feat: Conditonal Execution - Option 4 #2654): don't track it. Show the conditional UI on every task, always.
  • Option 1 — Derived (this PR): don't track it either, but infer it. A task is conditional when it has an isEnabled value — a literal on the task, or a connection to the reserved port.

Why derived

Two sources of truth drift, and this one drifts immediately. Pipelines don't only come from our editor — they come from the SDK, from other tools, from hand-written YAML. Those have isEnabled set and no annotation, so the annotation version opens them showing no condition and no edge: the connection is right there in the file and invisible on the canvas. The fix would be to backfill the annotation on import, which is deriving it anyway, one round trip late.

It keeps editor state out of the user's pipeline. Conditional-ness is already expressed by isEnabled. An annotation would write a second copy of it into the file for something we can compute.

Most tasks aren't conditional. Always-on avoids the state problem, but it spends vertical space on every node and adds a concept to every task for the benefit of the few that use it. A switch keeps it opt-in and keeps ordinary nodes looking ordinary.

One question, one answer. isTaskConditional(task, spec) is what the node, the panel and serialization all ask, so they can't disagree.

The honest cost: deriving couples the switch to the value. Turning it on has to write something ("true" — Always), and turning it off has to clear it, so "conditional but not configured yet" isn't a state we can represent. We think that's fine — Always is a sensible starting point, and the alternative was the drift above.

Also in here

  • The condition control was a three-way dropdown (Enable task: True / False / Conditional), which folded two different questions into one control — "should this be conditional" and "what's the condition". It's now a switch for the first and a toggle for the second.
  • Deleting the condition edge resets the task to enabled instead of resurfacing the previous literal. For an ordinary input, bringing back the old literal when you disconnect it is helpful. For a condition it isn't: resurfacing "false" would leave the task silently disabled after the user deliberately removed the condition.

Related Issue and Pull requests

Stacked on #2574. Alternatives: #2652 (annotation), #2654 (always-on). Followed by #2657, which finishes the UI.

Type of Change

  • New feature

Checklist

  • I have tested this does not break current pipelines / runs functionality
  • I have tested the changes on staging

Screenshots (if applicable)

Test Instructions

The conditional-execution flag gates all of this.

  1. Select a task → Config tab → toggle Conditional task on, then set the condition to False and confirm the task's isEnabled follows in the YAML.
  2. Toggle it back off and confirm isEnabled is cleared and no stray annotation is written.
  3. Connect a graph input or an upstream task output to the condition handle, then delete that edge — the task should stay conditional and reset to enabled.
  4. Import a pipeline whose YAML sets isEnabled to a reference (e.g. one produced by the SDK) and confirm the editor shows both the conditional UI and the edge without us having written anything into the file first.

Additional Comments

@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown

🎩 Preview

A preview build has been created at: 08-20-feat_conditional_execution_-_option_1/a2ba5f9

camielvs commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator Author

@camielvs
camielvs force-pushed the feat--Conditional-execution branch from 55706d9 to de8e0ac Compare August 20, 2026 17:28
@camielvs
camielvs force-pushed the 08-20-feat_conditional_execution_-_option_1 branch from f7393e4 to 9cd7b00 Compare August 20, 2026 17:28
@camielvs
camielvs force-pushed the 08-20-feat_conditional_execution_-_option_1 branch from 9cd7b00 to e672615 Compare August 20, 2026 17:51
@camielvs
camielvs force-pushed the 08-20-feat_conditional_execution_-_option_1 branch from e672615 to eaf0eb1 Compare August 20, 2026 18:38
@camielvs
camielvs force-pushed the feat--Conditional-execution branch from de8e0ac to bbfc2a7 Compare August 20, 2026 18:38
@camielvs
camielvs force-pushed the 08-20-feat_conditional_execution_-_option_1 branch from eaf0eb1 to d1187a7 Compare August 20, 2026 19:21
@camielvs
camielvs force-pushed the feat--Conditional-execution branch from bbfc2a7 to 7a522e2 Compare August 20, 2026 19:21
@camielvs
camielvs force-pushed the 08-20-feat_conditional_execution_-_option_1 branch from d1187a7 to 7175eea Compare August 20, 2026 19:25
@camielvs
camielvs force-pushed the feat--Conditional-execution branch from 7a522e2 to e4c7524 Compare August 20, 2026 19:25
@camielvs
camielvs force-pushed the 08-20-feat_conditional_execution_-_option_1 branch from 7175eea to 8907780 Compare August 20, 2026 19:30
@camielvs
camielvs force-pushed the feat--Conditional-execution branch from e4c7524 to 2b3d892 Compare August 20, 2026 19:30
@camielvs
camielvs force-pushed the 08-20-feat_conditional_execution_-_option_1 branch 2 times, most recently from e0c36ea to f920c2c Compare August 20, 2026 21:47
@camielvs
camielvs force-pushed the feat--Conditional-execution branch from 2b3d892 to 9c91bfc Compare August 20, 2026 21:47
Comment thread src/utils/conditionalExecution.ts
@camielvs
camielvs marked this pull request as ready for review August 21, 2026 17:26
@camielvs
camielvs requested a review from a team as a code owner August 21, 2026 17:26

camielvs commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator Author

Merge activity

  • Aug 24, 6:28 PM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Aug 24, 6:30 PM UTC: Graphite rebased this pull request as part of a merge.
  • Aug 24, 6:34 PM UTC: @camielvs merged this pull request with Graphite.

@camielvs
camielvs changed the base branch from feat--Conditional-execution to graphite-base/2651 August 24, 2026 18:28
@camielvs
camielvs changed the base branch from graphite-base/2651 to master August 24, 2026 18:28
@camielvs
camielvs force-pushed the 08-20-feat_conditional_execution_-_option_1 branch from f920c2c to a2ba5f9 Compare August 24, 2026 18:29
@camielvs
camielvs merged commit c81ecc9 into master Aug 24, 2026
17 checks passed
@camielvs
camielvs deleted the 08-20-feat_conditional_execution_-_option_1 branch August 24, 2026 18:34
camielvs added a commit that referenced this pull request Aug 24, 2026
## Description

Final pass over conditional execution. Nothing changes about what gets sent to the backend — this is UI, vocabulary and test coverage. The visual direction is borrowed from the exploration in #2649.

**"Run when", not "isEnabled".** `isEnabled` is spec jargon. The UI now says **Run when**, and the two literal choices read **Always** / **Never** rather than true/false.

**Conditional execution gets its own purple box in the Config tab.** A single switch turns it on for a task. When it's on, the **Run when** control appears inside the box — either the Always/Never toggle, or, if something is wired into it, the upstream source it follows (`→ Flag.flag`), formatted the same way bound inputs are formatted everywhere else in the editor.

**On the node, the condition is separate from the inputs.** It used to be injected into the task's input list as a fake input, which meant the list had to know about it in every place it did anything (splitting, condensing, counting). It's now its own purple row above the inputs with its own handle, which means:

- it stays visible when a node's inputs are condensed, instead of being counted into "+3 more"
- the input-list code went back to exactly what it is on master — the whole special case is gone
- collapsed nodes get the same treatment: a purple handle plus a branch icon

**Dragging off the "Run when" handle no longer creates a graph input called** **`__is_enabled__`.** The editor names an auto-created input after the port you dragged from, and the reserved port name was leaking into the user's pipeline. It now creates an input called `run_condition`, typed `String` — that's the form the condition is actually read in, and it's what lets the input connect to the ports components declare.

**Naming and comments.** Internal names now say what they are (`resetRunCondition`, `runConditionBinding`, `setRunCondition`, …). Comments that narrated the code were deleted; the ones left explain a decision the code can't.

**Tests.** New coverage for the shared helpers, the enable/disable actions (including that switching conditional execution off clears both the literal and the connection while leaving other connections alone), the node rendering, the auto-created input, and each of the fixes below.

### Fixes from review

- **A hand-written or SDK-generated pipeline can set the condition to an unquoted `false`.** That now reads as **Never**; before, it showed as **Always** while the backend skipped the task — the display and the behaviour disagreed.
- **A condition pointing at something that no longer exists** now shows what it pointed at, instead of quietly falling back to **Always**. Loading such a pipeline also keeps the condition rather than leaving the task ungated.
- **Grouping tasks into a subgraph, and ungrouping them again, no longer leaks the internal port name into the pipeline.** A promoted condition becomes a readable `run_condition` input, and a fixed condition survives the round trip.
- **Dragging off the handle of a task set to Never no longer ungates it.** The auto-created input now starts out holding the task's own condition, so the task keeps running when it was set to.
- **The Run when control is now properly labelled for screen readers.**
- One violet palette, one icon and one stated reason for both, so the condition row can't drift apart between the full and collapsed node.

## Related Issue and Pull requests

Stacked on #2651. UI direction borrowed from #2649. Followed by #2658, which stops conditional execution being set where the backend won't honour it.

## Type of Change

- [x] Improvement

## Checklist

- [ ] I have tested this does not break current pipelines / runs functionality
- [ ] I have tested the changes on staging

## Screenshots (if applicable)

![image.png](https://app.graphite.com/user-attachments/assets/867473a8-dde6-490d-b617-311386ab3a2f.png)

![image.png](https://app.graphite.com/user-attachments/assets/29830e75-b968-45dc-8052-8b3903daa8dc.png)

![image.png](https://app.graphite.com/user-attachments/assets/4b780085-7d4a-4f78-97da-504426663f52.png)

![image.png](https://app.graphite.com/user-attachments/assets/dbdeaf0d-4da1-45c9-b7fe-3f641e6a2cd9.png)



## Test Instructions

The `conditional-execution` flag is off by default — turn it on in Settings first.

1. Select a task → **Config** tab → toggle **Conditional execution**. The purple box expands to show **Run when**, and the node grows a purple row.
2. Flip **Run when** between Always and Never; the node's row should follow.
3. Condense the node's inputs — the condition row stays visible.
4. Drag from the node's purple handle onto empty canvas. You should get a `run_condition` String graph input, **not** one named `__is_enabled__`.
5. Set a task to **Never**, then drag off its handle. The new input should already hold `false`, and the task should still read as gated off — not silently switch to running.
6. Wire a task output into the handle. Both the node and the Config panel should read `→ Task.output`.
7. Delete that edge. The task stays conditional and resets to **Always** rather than silently staying Never.
8. Export the pipeline YAML and re-import it — the condition should survive the round trip.
9. Hand-edit a pipeline's YAML to set a task's condition to a bare `false` (no quotes), then open it. The task should read **Never**.
10. Select a conditional task and a neighbour, group them into a subgraph, then ungroup them. The condition should come back intact, and no input named `__is_enabled__` should appear anywhere.

## Additional Comments

Turning conditional execution off deliberately clears the condition (both the literal and any connection) rather than remembering it, so re-enabling starts from Always. That's the trade discussed in #2651.
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.

2 participants