Files
flowgraph/docs/architecture/decisions/001-ujsx-as-template-ir.md
glm-5.1 c5e649cc9f resolve mechanical architecture review issues (C-01,C-02,C-03,W-01,W-09,W-10,W-12)
- C-01: fix broken README link (call-graph-runtime.md → call-graph.md)
- C-02: add CallEdgeAttrs union type alias in schema.md
- C-03/W-12: rename TypedEdgeAttrs → OperationEdgeAttrs for consistent
  {GraphType}EdgeAttrs naming pattern, update all references
- W-01: standardize terminology — prerequisites=structural/graph,
  preconditions=reactive/computed, rename WorkflowNode.prerequisites
  to preconditions, rename computePrerequisites to computePreconditions
- W-09: update ADR-001/002/003 status from Proposed to Accepted
- W-10: clarify call graph mutation API — addCall creates triggered
  edges automatically, addDependency creates depends_on edges
- update review checklist with resolved items
2026-05-19 11:09:06 +00:00

3.8 KiB

ADR-001: ujsx Trees as Workflow Template IR

Status

Accepted

Context

Flowgraph needs a way to define workflow templates — reusable sequences of operations with conditional branching and parallel execution. The templates must be:

  1. Declarative — defining what should happen, not how
  2. Composable — nesting sequential, parallel, and conditional flows
  3. Serializable — store in JSON, transmit over APIs, version in git
  4. Validatable — check against an operation graph before execution
  5. Renderable to multiple targets — structural validation (DAG) and runtime execution (reactive)

The obvious approach is a custom template format: an array of step objects with type discriminators:

const template = [
  { type: "operation", name: "architect" },
  { type: "sequential", steps: [...] },
];

This works but has limitations:

  • Custom format requires a custom parser, serializer, and validator
  • No composition primitives — sequential and parallel are just types in an array
  • No host switching — a separate compiler is needed for each target (DAG, execution engine)
  • No incremental updates — changing a step requires rebuilding the entire structure

Decision

Use ujsx UNode trees as the workflow template intermediate representation. Workflow components (Operation, Sequential, Parallel, Conditional) are UComponent functions that produce UElement nodes. The template is rendered to different targets through ujsx HostConfig implementations.

const template = h(Sequential, {},
  h(Operation, { name: "architect" }),
  h(Operation, { name: "reviewer" }),
);

Rationale

  1. No new format — ujsx already defines UNode, UElement, URoot, type guards, and serialization. We don't need to design, implement, and maintain a template format.

  2. Composition is structural<Sequential> and <Parallel> compose naturally as parent-child structure in a tree. Array-of-objects requires custom merging logic.

  3. Host target switching — the same UNode tree renders to a graphology DAG (for validation) or a reactive engine (for execution) by swapping the HostConfig. No template-specific compiler needed.

  4. Incremental updates — when the ujsx reconciler is implemented, template changes (add/remove/reorder steps) can be applied incrementally without rebuilding the entire DAG. Array-of-objects requires full diffing and rebuilding.

  5. Reactive props@preact/signals-core enables signal-driven prop updates. An Operation node's name could be a signal<string>, enabling dynamic workflow modification at runtime.

  6. Serialization for freeUNode trees are plain JSON. JSON.stringify(template) works. No custom serializer needed.

Consequences

  • Direct dependency on @alkdev/ujsx — flowgraph imports h, createRoot, HostConfig, ReactiveRoot, and type definitions from ujsx. This is a direct dependency, not a peer dependency.
  • Function props don't serializeConditional.test can be a function (results) => boolean, which doesn't survive JSON round-trips. Templates with conditional branches need to provide test at render time or use expression strings.
  • Template components must follow ujsx component contract(props) => UNode. This is a minimal contract but it means components are synchronous functions that return a tree.
  • The template IS the tree — there is no separate compilation step between the ujsx tree and the render target. The HostConfig.render() call IS the compilation.

References

  • ujsx architecture: @alkdev/ujsx/docs/architecture/README.md
  • ujsx HostConfig: @alkdev/ujsx/docs/architecture/host-config.md
  • Workflow templates: workflow-templates.md
  • Host configs: host-configs.md