Phase 1: Workflow versioning + typed flow inputs #5

Open
opened 2026-04-19 16:31:13 +00:00 by timur · 2 comments
Owner

Context

Hero_logic currently treats every variation of a flow as a separate Workflow record. When optimize_flow finds a better model configuration, it mutates the live hero_proc action configs directly — there's no version history, no rollback, no way to compare service_agent_v3 baseline vs an optimized variant side by side.

Additionally, flow inputs are implicit: whatever JSON you pass to play_start.input_data becomes a dict that nodes can reference via {{prompt}}, {{scripts_dir}} etc. The convention is {{inputs.x}} but there's no schema, no validation, no editor autocomplete, and no way to know what a workflow expects without reading its action scripts.

This issue is the foundation for Phases 2-4 (Examples rework, Benchmark flow, Version router). Ship this before touching anything else.

Goal

  1. Workflow is a container of Versions. One Workflow has many WorkflowVersion snapshots. Each version owns its own nodes/edges/actions. A current_version_sid points to the active version. Plays run against a specific version.
  2. Flow inputs are first-class typed declarations. A workflow declares its inputs as a list of {name, type, description, required, default} entries. Nodes reference them via the existing {{name}} templating, now validated at play_start.

OSchema changes

File: crates/hero_logic/schemas/logic/logic.oschema

Add new types:

# FlowInput declares a typed input parameter for a workflow.
FlowInput = {
    name: str                       # identifier used in {{name}} templating
    type: str                       # string|number|integer|boolean|object|array
    description: str
    required: bool
    default: str                    # serialized default value (JSON), empty if none
}

# WorkflowVersion holds a snapshot of the workflow's mutable content. [rootobject]
WorkflowVersion = {
    sid: str
    workflow_sid: str               # parent workflow
    version_label: str              # e.g. "v1", "v3.1-optimized", user-assigned
    notes: str                      # why this version exists ("auto-optimized for speed")
    created_at: int
    nodes: [Node]
    edges: [Edge]
}

Refactor existing Workflow:

# Workflow is now a container of versioned content + declared inputs. [rootobject]
Workflow = {
    sid: str
    name: str
    description: str
    tags: [str]
    inputs: [FlowInput]             # NEW — typed input declarations
    current_version_sid: str        # NEW — SID of the active WorkflowVersion
    versions: [str]                 # NEW — SIDs of all versions, newest first
    created_at: int
    updated_at: int
    # REMOVED: nodes, edges, version (string), input_schema, output_schema
}

The Play schema gets a new field pinning it to a version:

Play = {
    ...
    workflow_sid: str
    workflow_version_sid: str       # NEW — which version was executed
    ...
}

Actions configs (model, prompts) for AI nodes continue to live in hero_proc. A version's nodes carry an action_name reference; the action config itself is NOT stored per-version yet. This means switching active version doesn't automatically change model configs — optimize_flow needs to produce versions with distinct action_name suffixes (e.g., logic.agent_v3.code_generation@v2) OR a future issue addresses this. For now, phase 1 just does structural versioning; action-config versioning is a follow-up.

Code changes

crates/hero_logic/src/engine/executor.rs

  • PlayExecutor::execute(play_sid) currently does workflow_get(play.workflow_sid). Change to: resolve play.workflow_version_sid, then workflow_version_get(version_sid) to get nodes/edges.
  • If play.workflow_version_sid is empty (old play), fall back to workflow's current_version_sid.

crates/hero_logic/src/logic/server/rpc.rs

Existing RPC method changes:

  • workflow_from_template: Load template JSON. Create Workflow record with declared inputs. Create WorkflowVersion v1 with the nodes/edges. Set current_version_sid. Return the Workflow.
  • play_start: Accept optional workflow_version_sid; if absent, use workflow's current_version_sid. Validate input_data against the workflow's declared inputs (required inputs present, types roughly match). Reject with clear error on missing required.

New RPC methods:

  • workflow_create_version(workflow_sid, from_version_sid?, notes) -> WorkflowVersion: Clone an existing version (or current if unspecified). Returns the new version. Does not set as current.
  • workflow_set_current_version(workflow_sid, version_sid): Switch the active version.
  • workflow_version_get(sid): Fetch a specific version.
  • workflow_version_list_for_workflow(workflow_sid): List all versions for a workflow.

crates/hero_logic/src/engine/template_loader.rs

Template JSON schema changes (additive; old format still loads):

  • Top-level inputs: [{name, type, description, required, default}] (optional; defaults to auto-inferred from first node's input_schema for compat)
  • Top-level nodes, edges, actions still work, but now they're loaded into an initial v1 WorkflowVersion

When loading a template:

  1. Create Workflow with declared inputs
  2. Create WorkflowVersion v1 with nodes/edges
  3. Register action specs with hero_proc (unchanged)
  4. Link v1 as current_version_sid

Template JSON files

Update both templates to declare inputs:

crates/hero_logic/templates/service_agent_v3.json:

{
  "name": "Service Agent v3",
  "version_label": "v1",
  "inputs": [
    {"name": "prompt", "type": "string", "description": "User request", "required": true, "default": ""},
    {"name": "model", "type": "string", "description": "LLM override", "required": false, "default": ""}
  ],
  "nodes": [...],
  "edges": [...],
  "actions": [...]
}

crates/hero_logic/templates/optimize_flow.json: declare target_template, num_tests, test_prompts as typed inputs.

Migration

For existing Workflow records without versions/current_version_sid:

  • On read, detect legacy shape (has inline nodes/edges); synthesize a v1 WorkflowVersion on the fly; write the migrated structure back on next save.
  • Provide a one-shot migration RPC workflow_migrate_legacy(workflow_sid) that converts one record and is idempotent.

UI changes

File: crates/hero_logic_ui/templates/workflow_editor.html + JS

  • Version selector dropdown in the editor header: lists all versions, highlights current, "Create new version" button clones current.
  • Inputs panel — a new left sidebar section where users add/edit/remove typed inputs (name, type dropdown, description, required checkbox, default value).
  • Play start form renders fields for each declared input (text/number/checkbox by type) instead of a free-form JSON textarea.
  • Workflow card in dashboard shows {wf.name} — {wf.versions.len()} versions.

Acceptance criteria

  • Workflow record has inputs, versions, current_version_sid fields; nodes/edges removed from Workflow
  • WorkflowVersion root object exists with its own SID space
  • Loading service_agent_v3 template creates a Workflow with declared inputs + v1 WorkflowVersion with nodes/edges
  • play_start validates input_data against declared inputs (rejects missing required with clear error)
  • workflow_create_version clones the current version successfully; each version is independently executable
  • Play records carry workflow_version_sid and execute against that specific version
  • Legacy workflows (pre-migration) still load and execute (auto-migrated to v1 on read)
  • UI shows version dropdown and inputs panel; existing service_agent_v3 works end-to-end through the UI
  • optimize_flow still runs against service_agent_v3 (may need compat shim if it calls workflow_from_template which now returns a different shape)

Dependencies

None. This is the foundation for Phases 2-4.

Out of scope (follow-up)

  • Per-version action config (storing model/prompt choices in the WorkflowVersion rather than mutating hero_proc live) — big refactor, separate issue
  • UI for editing individual actions within a version
  • Version comparison / diff view
  • Soft-delete of versions
## Context Hero_logic currently treats every variation of a flow as a separate `Workflow` record. When `optimize_flow` finds a better model configuration, it mutates the live hero_proc action configs directly — there's no version history, no rollback, no way to compare `service_agent_v3` baseline vs an optimized variant side by side. Additionally, flow inputs are implicit: whatever JSON you pass to `play_start.input_data` becomes a dict that nodes can reference via `{{prompt}}`, `{{scripts_dir}}` etc. The convention is `{{inputs.x}}` but there's no schema, no validation, no editor autocomplete, and no way to know what a workflow expects without reading its action scripts. This issue is the **foundation** for Phases 2-4 (Examples rework, Benchmark flow, Version router). Ship this before touching anything else. ## Goal 1. **Workflow is a container of Versions.** One `Workflow` has many `WorkflowVersion` snapshots. Each version owns its own nodes/edges/actions. A `current_version_sid` points to the active version. Plays run against a specific version. 2. **Flow inputs are first-class typed declarations.** A workflow declares its inputs as a list of `{name, type, description, required, default}` entries. Nodes reference them via the existing `{{name}}` templating, now validated at play_start. ## OSchema changes File: `crates/hero_logic/schemas/logic/logic.oschema` Add new types: ``` # FlowInput declares a typed input parameter for a workflow. FlowInput = { name: str # identifier used in {{name}} templating type: str # string|number|integer|boolean|object|array description: str required: bool default: str # serialized default value (JSON), empty if none } # WorkflowVersion holds a snapshot of the workflow's mutable content. [rootobject] WorkflowVersion = { sid: str workflow_sid: str # parent workflow version_label: str # e.g. "v1", "v3.1-optimized", user-assigned notes: str # why this version exists ("auto-optimized for speed") created_at: int nodes: [Node] edges: [Edge] } ``` Refactor existing `Workflow`: ``` # Workflow is now a container of versioned content + declared inputs. [rootobject] Workflow = { sid: str name: str description: str tags: [str] inputs: [FlowInput] # NEW — typed input declarations current_version_sid: str # NEW — SID of the active WorkflowVersion versions: [str] # NEW — SIDs of all versions, newest first created_at: int updated_at: int # REMOVED: nodes, edges, version (string), input_schema, output_schema } ``` The `Play` schema gets a new field pinning it to a version: ``` Play = { ... workflow_sid: str workflow_version_sid: str # NEW — which version was executed ... } ``` Actions configs (model, prompts) for AI nodes continue to live in hero_proc. **A version's nodes carry an `action_name` reference; the action config itself is NOT stored per-version yet.** This means switching active version doesn't automatically change model configs — `optimize_flow` needs to produce versions with distinct action_name suffixes (e.g., `logic.agent_v3.code_generation@v2`) OR a future issue addresses this. For now, phase 1 just does structural versioning; action-config versioning is a follow-up. ## Code changes ### `crates/hero_logic/src/engine/executor.rs` - `PlayExecutor::execute(play_sid)` currently does `workflow_get(play.workflow_sid)`. Change to: resolve `play.workflow_version_sid`, then `workflow_version_get(version_sid)` to get nodes/edges. - If `play.workflow_version_sid` is empty (old play), fall back to workflow's `current_version_sid`. ### `crates/hero_logic/src/logic/server/rpc.rs` Existing RPC method changes: - `workflow_from_template`: Load template JSON. Create Workflow record with declared inputs. Create WorkflowVersion v1 with the nodes/edges. Set `current_version_sid`. Return the Workflow. - `play_start`: Accept optional `workflow_version_sid`; if absent, use workflow's `current_version_sid`. Validate `input_data` against the workflow's declared `inputs` (required inputs present, types roughly match). Reject with clear error on missing required. New RPC methods: - `workflow_create_version(workflow_sid, from_version_sid?, notes) -> WorkflowVersion`: Clone an existing version (or current if unspecified). Returns the new version. Does not set as current. - `workflow_set_current_version(workflow_sid, version_sid)`: Switch the active version. - `workflow_version_get(sid)`: Fetch a specific version. - `workflow_version_list_for_workflow(workflow_sid)`: List all versions for a workflow. ### `crates/hero_logic/src/engine/template_loader.rs` Template JSON schema changes (additive; old format still loads): - Top-level `inputs: [{name, type, description, required, default}]` (optional; defaults to auto-inferred from first node's input_schema for compat) - Top-level `nodes`, `edges`, `actions` still work, but now they're loaded into an initial `v1` WorkflowVersion When loading a template: 1. Create Workflow with declared inputs 2. Create WorkflowVersion v1 with nodes/edges 3. Register action specs with hero_proc (unchanged) 4. Link v1 as current_version_sid ### Template JSON files Update both templates to declare inputs: `crates/hero_logic/templates/service_agent_v3.json`: ```json { "name": "Service Agent v3", "version_label": "v1", "inputs": [ {"name": "prompt", "type": "string", "description": "User request", "required": true, "default": ""}, {"name": "model", "type": "string", "description": "LLM override", "required": false, "default": ""} ], "nodes": [...], "edges": [...], "actions": [...] } ``` `crates/hero_logic/templates/optimize_flow.json`: declare `target_template`, `num_tests`, `test_prompts` as typed inputs. ### Migration For existing Workflow records without `versions`/`current_version_sid`: - On read, detect legacy shape (has inline `nodes`/`edges`); synthesize a v1 WorkflowVersion on the fly; write the migrated structure back on next save. - Provide a one-shot migration RPC `workflow_migrate_legacy(workflow_sid)` that converts one record and is idempotent. ## UI changes File: `crates/hero_logic_ui/templates/workflow_editor.html` + JS - **Version selector dropdown** in the editor header: lists all versions, highlights current, "Create new version" button clones current. - **Inputs panel** — a new left sidebar section where users add/edit/remove typed inputs (name, type dropdown, description, required checkbox, default value). - **Play start form** renders fields for each declared input (text/number/checkbox by type) instead of a free-form JSON textarea. - Workflow card in dashboard shows `{wf.name} — {wf.versions.len()} versions`. ## Acceptance criteria - [ ] `Workflow` record has `inputs`, `versions`, `current_version_sid` fields; `nodes`/`edges` removed from Workflow - [ ] `WorkflowVersion` root object exists with its own SID space - [ ] Loading `service_agent_v3` template creates a Workflow with declared inputs + v1 WorkflowVersion with nodes/edges - [ ] `play_start` validates input_data against declared inputs (rejects missing required with clear error) - [ ] `workflow_create_version` clones the current version successfully; each version is independently executable - [ ] Play records carry `workflow_version_sid` and execute against that specific version - [ ] Legacy workflows (pre-migration) still load and execute (auto-migrated to v1 on read) - [ ] UI shows version dropdown and inputs panel; existing service_agent_v3 works end-to-end through the UI - [ ] `optimize_flow` still runs against service_agent_v3 (may need compat shim if it calls `workflow_from_template` which now returns a different shape) ## Dependencies None. This is the foundation for Phases 2-4. ## Out of scope (follow-up) - Per-version action config (storing model/prompt choices in the WorkflowVersion rather than mutating hero_proc live) — big refactor, separate issue - UI for editing individual actions within a version - Version comparison / diff view - Soft-delete of versions
Author
Owner

Core foundation landed: d5b3c95

Implemented the backend schema + executor changes for Phase 1. Still TODO: UI (version selector, inputs panel, typed run form) — tracked as a remaining checklist item below.

What's live

OSchema:

  • FlowInput type: {name, input_type, description, required, default}
  • WorkflowVersion root object with its own SID space
  • Workflow refactored (no more inline nodes/edges; has inputs, current_version_sid, versions)
  • Play.workflow_version_sid pins plays to a specific version

RPC methods:

  • workflow_create_version(workflow_sid, from_version_sid, notes) — clones a version (default: current), auto-picks next label (v1→v2→v3…)
  • workflow_set_current_version(workflow_sid, version_sid) — verifies the version belongs to this workflow before switching
  • workflow_version_list_for_workflow(workflow_sid) — returns SIDs newest-first
  • workflow_migrate_legacy(workflow_sid) — idempotent; creates v1 if none

Behavior changes:

  • workflow_from_template creates Workflow + initial WorkflowVersion atomically
  • workflow_save_from_json creates a NEW version on update (preserves history)
  • play_start validates input_data against declared inputs; rejects missing required with a clear error; pins the Play to the workflow's current_version_sid
  • Executor resolves play.workflow_version_sid first, falls back to workflow.current_version_sid (defensive for pre-Phase-1 plays)
  • validate_workflow(workflow, version) now takes both

Templates updated:

  • service_agent_v3.json: declares prompt (required), model (optional)
  • optimize_flow.json: declares target_template, num_tests, test_prompts

Verified end-to-end

$ workflow_from_template('service_agent_v3')
→ {sid: 00c4, name: 'Service Agent v3',
   inputs: [{name:'prompt',required:true}, {name:'model',required:false}],
   current_version_sid: '00c5', versions: ['00c5']}

$ play_start(wf='00c4', input_data='{}')
→ error: Missing required input 'prompt'  ✓

$ play_start(wf='00c4', input_data='{"prompt":"ping hero_proc"}')
→ {sid: 00c6, workflow_version_sid: '00c5'}  ✓
→ polls to status=success  ✓

$ workflow_create_version(wf='00c4', from_version_sid='', notes='manual v2 test')
→ {sid: 00c7, version_label: 'v2', nodes copied: 7}  ✓

$ workflow_version_list_for_workflow(wf='00c4')
→ ['00c7', '00c5']  ✓

Remaining for this issue

  • Workflow schema change
  • WorkflowVersion root object
  • Template loader splits into Workflow + v1 WorkflowVersion
  • workflow_from_template creates both atomically
  • play_start validates against inputs, pins version
  • workflow_create_version clones current
  • Play records workflow_version_sid
  • Executor fetches WorkflowVersion.nodes/edges
  • service_agent_v3 + optimize_flow declare typed inputs
  • UI: version selector dropdown in editor
  • UI: inputs panel in editor (add/edit/remove typed inputs)
  • UI: play-start form renders fields per declared input (not free-form JSON)
  • Dashboard card shows {n} versions
  • Verify optimize_flow still passes with new play_start validation

I'll open a follow-up issue for the UI work if it blocks moving to Phase 2, since #6/#7/#8 don't hard-depend on the UI changes (they're all backend).

## Core foundation landed: d5b3c95 Implemented the backend schema + executor changes for Phase 1. Still TODO: **UI (version selector, inputs panel, typed run form)** — tracked as a remaining checklist item below. ### What's live **OSchema:** - `FlowInput` type: `{name, input_type, description, required, default}` - `WorkflowVersion` root object with its own SID space - `Workflow` refactored (no more inline nodes/edges; has `inputs`, `current_version_sid`, `versions`) - `Play.workflow_version_sid` pins plays to a specific version **RPC methods:** - `workflow_create_version(workflow_sid, from_version_sid, notes)` — clones a version (default: current), auto-picks next label (v1→v2→v3…) - `workflow_set_current_version(workflow_sid, version_sid)` — verifies the version belongs to this workflow before switching - `workflow_version_list_for_workflow(workflow_sid)` — returns SIDs newest-first - `workflow_migrate_legacy(workflow_sid)` — idempotent; creates v1 if none **Behavior changes:** - `workflow_from_template` creates Workflow + initial WorkflowVersion atomically - `workflow_save_from_json` creates a NEW version on update (preserves history) - `play_start` validates `input_data` against declared `inputs`; rejects missing required with a clear error; pins the Play to the workflow's `current_version_sid` - Executor resolves `play.workflow_version_sid` first, falls back to `workflow.current_version_sid` (defensive for pre-Phase-1 plays) - `validate_workflow(workflow, version)` now takes both **Templates updated:** - `service_agent_v3.json`: declares `prompt` (required), `model` (optional) - `optimize_flow.json`: declares `target_template`, `num_tests`, `test_prompts` ### Verified end-to-end ``` $ workflow_from_template('service_agent_v3') → {sid: 00c4, name: 'Service Agent v3', inputs: [{name:'prompt',required:true}, {name:'model',required:false}], current_version_sid: '00c5', versions: ['00c5']} $ play_start(wf='00c4', input_data='{}') → error: Missing required input 'prompt' ✓ $ play_start(wf='00c4', input_data='{"prompt":"ping hero_proc"}') → {sid: 00c6, workflow_version_sid: '00c5'} ✓ → polls to status=success ✓ $ workflow_create_version(wf='00c4', from_version_sid='', notes='manual v2 test') → {sid: 00c7, version_label: 'v2', nodes copied: 7} ✓ $ workflow_version_list_for_workflow(wf='00c4') → ['00c7', '00c5'] ✓ ``` ### Remaining for this issue - [x] `Workflow` schema change - [x] `WorkflowVersion` root object - [x] Template loader splits into Workflow + v1 WorkflowVersion - [x] `workflow_from_template` creates both atomically - [x] `play_start` validates against `inputs`, pins version - [x] `workflow_create_version` clones current - [x] Play records `workflow_version_sid` - [x] Executor fetches WorkflowVersion.nodes/edges - [x] service_agent_v3 + optimize_flow declare typed inputs - [ ] **UI: version selector dropdown in editor** - [ ] **UI: inputs panel in editor (add/edit/remove typed inputs)** - [ ] **UI: play-start form renders fields per declared input (not free-form JSON)** - [ ] **Dashboard card shows `{n} versions`** - [ ] Verify optimize_flow still passes with new play_start validation I'll open a follow-up issue for the UI work if it blocks moving to Phase 2, since #6/#7/#8 don't hard-depend on the UI changes (they're all backend).
Author
Owner

Phase 1 UI landed (commit 68b8153)

Editor at /workflows/:sid now exposes versioning + typed inputs:

  • Version selector dropdown in topbar (switches current_version_sid via workflow_set_current_version, reloads editor against the chosen version).
  • New version button → modal that clones current into a new v2/v3/... with optional notes + auto-switch.
  • Inputs panel in the right sidebar renders declared FlowInputs (name, type, required badge, default).
  • Start play button opens a modal that renders one typed form field per declared input (string=text, number/integer=number, boolean=select, object/array=JSON textarea). Submit calls logicservice.play_start with properly-typed input_data.
  • routes.rs::workflow_editor_handler now fetches the current WorkflowVersion via workflow_version_fetch (JSON-native wrapper around OSIS OTOML) and passes inputs, current_version_sid, versions_meta into the template alongside the flattened nodes/edges the existing Cytoscape code already expects.
  • Schema hotfix: new scalar fields (current_version_sid, workflow_version_sid, input_values, input_data) now declare = "" defaults so legacy pre-Phase-1 records deserialize cleanly instead of erroring with missing field.

Verified:

  • Fresh workflow_from_template("service_agent_v3") → workflow persists with current_version_sid + versions:[v1] + inputs:[prompt, model]
  • workflow_version_fetch returns WorkflowVersion with 7 nodes / 9 edges as JSON
  • Editor page renders all Phase 1 UI markup with injected window.INITIAL_WORKFLOW.{inputs, current_version_sid, versions_meta}

Still open for this issue:

  • Browser-test switch-version / create-version / start-play round trips
  • Detect stale Play records referencing a deleted version and surface a warning
  • Ensure workflow_save_from_json edits target the current version (not the Workflow container)
### Phase 1 UI landed (commit 68b8153) Editor at `/workflows/:sid` now exposes versioning + typed inputs: - **Version selector** dropdown in topbar (switches `current_version_sid` via `workflow_set_current_version`, reloads editor against the chosen version). - **New version** button → modal that clones current into a new v2/v3/... with optional notes + auto-switch. - **Inputs panel** in the right sidebar renders declared `FlowInput`s (name, type, required badge, default). - **Start play** button opens a modal that renders one typed form field per declared input (string=text, number/integer=number, boolean=select, object/array=JSON textarea). Submit calls `logicservice.play_start` with properly-typed `input_data`. - `routes.rs::workflow_editor_handler` now fetches the current `WorkflowVersion` via `workflow_version_fetch` (JSON-native wrapper around OSIS OTOML) and passes `inputs`, `current_version_sid`, `versions_meta` into the template alongside the flattened nodes/edges the existing Cytoscape code already expects. - Schema hotfix: new scalar fields (`current_version_sid`, `workflow_version_sid`, `input_values`, `input_data`) now declare `= ""` defaults so legacy pre-Phase-1 records deserialize cleanly instead of erroring with `missing field`. **Verified:** - Fresh `workflow_from_template("service_agent_v3")` → workflow persists with `current_version_sid` + `versions:[v1]` + `inputs:[prompt, model]` - `workflow_version_fetch` returns `WorkflowVersion` with 7 nodes / 9 edges as JSON - Editor page renders all Phase 1 UI markup with injected `window.INITIAL_WORKFLOW.{inputs, current_version_sid, versions_meta}` **Still open for this issue:** - Browser-test switch-version / create-version / start-play round trips - Detect stale Play records referencing a deleted version and surface a warning - Ensure `workflow_save_from_json` edits target the current version (not the Workflow container)
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_logic#5
No description provided.