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. This guide describes current UWS 1.12 behavior, with version gates preserved for older documents.

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)
}

A runtime may also implement the additive uws1.RuntimeWithResult interface:

type RuntimeWithResult interface {
    ExecuteLeafWithResult(ctx context.Context, op *Operation) (any, error)
}

When present, the orchestrator stores the returned JSON-compatible leaf result on the operation execution record before evaluating success criteria and outputs. This makes $response available to those expressions. For a step that directly invokes an operation, the same result is copied to the step record so step outputs can read it. Existing runtimes that implement only Runtime remain valid and continue using ExecuteLeaf; the core receives no return value from that method.

The mockruntime package implements both interfaces. Its response record shape follows the operation profile and is not a UWS wire contract.

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().

UWS 1.9.1 and later content-trust analysis is intentionally not a fourth automatic check. Applications invoke contenttrust.Analyze explicitly before or after validation when they want advisory provenance findings. The analyzer never changes execution records, runtime inputs, orchestration, authorization, or executor results. See the UWS 1.12 contract and content-trust guide.

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 keys shown above are from this Go implementation only. UWS 1.11 established that record keys, persistence, and serialized history are implementation-defined, while defining portable loop, forEach, and merge result payload shapes; UWS 1.12 preserves those rules. See Structural Results and the UWS 1.12 execution contract.

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, zero-based index, batch, zero-based batch index, and position in batch; $batchIndex is available only in a UWS 1.11-or-later loop
Input bindings Current $inputs values, including step-local bindings when a step supplies inputs
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
}

UWS 1.12 adds declarative operation effects (read, write, or unknown; omission means unknown) and pending step contracts for work that has not been bound to an operation. Pending input and output declarations are recursive object-root schemas, not runtime values. Validate() accepts a valid pending contract, while executable validation rejects any pending step before calling the runtime, even in a branch execution would not select. An effect label is descriptive; it does not authorize a live call. The pending-only example shows recursive field sets. See the UWS 1.12 contract.

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()  →  optional Analyze()  →  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 →