Skip to content

Feature 7: Execution Model

Success Criteria and Actions | Next: Extension Profiles →


UWS defines a real orchestrator/runtime split. This is what distinguishes UWS from a document-only format: the specification defines portable execution semantics, not just document shape.

The Split

UWS core (the Orchestrator) owns:

  • Document validation before execution
  • Indexing operations, workflows, steps, trigger targets, and parallelGroup members
  • Dependency execution across operations, workflows, and steps
  • Evaluation and enforcement of dependsOn, when, forEach, items, batchSize, wait
  • Execution of all six structural constructs
  • Processing of success and failure actions (retry, goto, end)
  • Output resolution and structural result shaping
  • Trigger route resolution and routed execution

The bound runtime owns:

  • Leaf execution of a single Operation (one HTTP call, or one extension-owned action)
  • Expression evaluation against the current execution context
  • Resolving the item list for forEach and loop

This line is what allows UWS to define portable orchestration semantics without standardizing HTTP clients, secret handling, storage engines, or product-specific runtime hooks.

The Runtime Interface

Three methods are all the orchestrator needs from a concrete runtime:

type Runtime interface {
    ExecuteLeaf(ctx context.Context, op *Operation) error
    EvaluateExpression(ctx context.Context, expr string) (any, error)
    ResolveItems(ctx context.Context, itemsExpr string) ([]any, error)
}

Everything above this line — dependency resolution, parallel scheduling, retry counting, switch evaluation, loop batching — is owned by UWS core.

Example 1: Minimal Runtime Implementation

A mock runtime that records calls, useful for testing:

type MockRuntime struct {
    Calls  []string
    Values map[string]any
}

func (r *MockRuntime) ExecuteLeaf(ctx context.Context, op *uws1.Operation) error {
    r.Calls = append(r.Calls, op.OperationID)
    return nil
}

func (r *MockRuntime) EvaluateExpression(ctx context.Context, expr string) (any, error) {
    if v, ok := r.Values[expr]; ok {
        return v, nil
    }
    return nil, nil
}

func (r *MockRuntime) ResolveItems(ctx context.Context, itemsExpr string) ([]any, error) {
    v, err := r.EvaluateExpression(ctx, itemsExpr)
    if v == nil {
        return nil, err
    }
    items, ok := v.([]any)
    if !ok {
        return nil, fmt.Errorf("items expression did not resolve to array")
    }
    return items, nil
}

Example 2: Binding and Executing a Document

package main

import (
    "context"
    "log"
    "os"

    "github.com/OpenUdon/uws/convert"
    "github.com/OpenUdon/uws/uws1"
)

func main() {
    data, err := os.ReadFile("workflow.uws.yaml")
    if err != nil {
        log.Fatal(err)
    }

    var doc uws1.Document
    if err := convert.UnmarshalYAML(data, &doc); err != nil {
        log.Fatal(err)
    }

    rt := &MyHTTPRuntime{ /* ... */ }
    doc.SetRuntime(rt)

    ctx := context.Background()
    if err := doc.Execute(ctx); err != nil {
        log.Fatalf("execution failed: %v", err)
    }

    // Inspect what ran
    for key, record := range doc.ExecutionRecords() {
        log.Printf("%s: status=%s", key, record.Status)
    }
}

doc.Execute(ctx) runs three checks automatically before handing off to the orchestrator: Validate(), ValidateExecutable(), and ValidateExecutionEntrypoint().

Example 3: Trigger Dispatch

Accepting an inbound event and routing it through UWS core:

// Receive a webhook payload (e.g. from an HTTP handler)
func handleWebhook(w http.ResponseWriter, r *http.Request) {
    var payload map[string]any
    json.NewDecoder(r.Body).Decode(&payload)

    // Determine which output fired based on payload content
    outputIndex := 0  // "created"
    if payload["event"] == "updated" {
        outputIndex = 1
    }

    err := doc.DispatchTrigger(r.Context(), "order_events", outputIndex, payload)
    if err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    w.WriteHeader(204)
}

UWS core resolves the route, validates the targets, and executes them through the standard orchestrator. The payload becomes $trigger inside the routed workflows.

Example 4: Inspecting Execution Records

After execution completes, records identify every step that ran:

doc.SetRuntime(rt)
if err := doc.Execute(ctx); err != nil {
    log.Printf("execution error: %v", err)
}

records := doc.ExecutionRecords()
for key, record := range records {
    fmt.Printf("%-40s status=%-10s kind=%s\n",
        key, record.Status, record.Kind)
}

Example output:

op:list_pets                   status=success    kind=operation
op:send_report                 status=success    kind=operation
wf:main                        status=success    kind=workflow

The keying scheme is implementation-defined. UWS 1.0 does not standardize a serialized record store.

Execution Context Available to the Runtime

During each ExecuteLeaf call, the runtime can inspect:

Context What it contains
Trigger context Active trigger ID, emitted output label, and payload
Iteration context Current item value, index, batch number, position in batch
Current-execution context The operation or step being run right now
Execution records snapshot All records accumulated so far

These are passed through context.Context and are never serialized into the UWS wire format.

Validation Before Execution

The three pre-execution checks run automatically inside doc.Execute(ctx):

if err := d.Validate(); err != nil {
    return err   // UWS document validity
}
if err := d.ValidateExecutable(); err != nil {
    return err   // entry workflow, runtime binding
}
if err := d.ValidateExecutionEntrypoint(); err != nil {
    return err   // entrypoint requirements
}

The AI Agent Pipeline

For AI agent use cases, UWS acts as the contract between intent extraction and execution:

natural language  →  structured intent  →  UWS document  →  Validate()  →  Execute()
// Agent proposes a UWS document
proposedYAML := agent.ProduceWorkflow(userIntent)

var doc uws1.Document
convert.UnmarshalYAML([]byte(proposedYAML), &doc)

// Catch errors before execution
result := doc.ValidateResult()
if !result.Valid() {
    // Feed structured errors back to the model for correction
    correction := agent.CorrectWorkflow(proposedYAML, result.Errors)
    // ... retry
    return
}

// Only execute a validated document
doc.SetRuntime(rt)
doc.Execute(ctx)

ValidateResult() returns path-tagged errors that drop straight into a corrective prompt. Two fields, two exact paths — enough for the model to fix in one pass rather than guessing from prose.

From The Big Fixture

The large fixture is built from Go structs, validated, serialized, and round-tripped across JSON, YAML, and HCL:

doc := BuildDocument()
if err := doc.Validate(); err != nil {
    return fmt.Errorf("validate source document: %w", err)
}

jsonData, err := convert.MarshalJSONIndent(doc, "", "  ")
hclData, err := convert.MarshalHCL(doc)
yamlData, err := convert.MarshalYAML(doc)
if err := verifyRoundTrips(jsonData, hclData, yamlData); err != nil {
    return err
}

Full context: testdata/big/main.go.


Success Criteria and Actions | Next: Extension Profiles →