--- title: "Design and Implementation of lda.chat: An AI Agent for Automating and Creating Workspace Workflows" subtitle: "" date: "July 1, 2026" lang: "en-US" documentclass: report papersize: a4 fontsize: 10pt toc: true toc-depth: 2 lof: true lot: true numbersections: true bibliography: references.bib link-citations: true figureTitle: "Figure" figPrefix: "Figure" chapters: true appendix: true syntax-highlighting: idiomatic geometry: - top=30mm - bottom=30mm - left=32mm - right=32mm mainfont: "Libertinus Serif" sansfont: "Libertinus Sans" monofont: "Libertinus Mono" mathfont: "Libertinus Math" colorlinks: true linkcolor: "MidnightBlue" urlcolor: "MidnightBlue" toccolor: "MidnightBlue" keywords: - workflow - agents - source providers - JSON-RPC - MCP - Python sources header-includes: # you can not specify -H and this at the same time. diagram: engine: mermaid: theme: neutral --- # List of Abbreviations {.unnumbered} | Abbreviation | Meaning | | --- | --- | | API | Application Programming Interface | | AI | Artificial Intelligence | | CLI | Command-Line Interface | | JSON-RPC | JavaScript Object Notation Remote Procedure Call | | LLM | Large Language Model | | MCP | Model Context Protocol | | RPC | Remote Procedure Call | | USTH | University of Science and Technology of Hanoi | : Abbreviations used in the thesis. {#tbl:abbreviations .unnumbered} # Abstract {.unnumbered} Preparing reports, transforming documents, and collecting workspace information often involve procedures that must be repeated with new inputs. An AI assistant can help perform such work, but a successful conversation does not itself preserve an executable procedure. This thesis presents `lda.chat`, a programmable workflow platform for defining, checking, running, and inspecting reusable workspace procedures. A workflow describes the operations to perform, the data they exchange, and the decisions that select the next step. An **artifact** is an immutable saved version of that workflow. A **deployment** connects a saved version to the concrete services it will use. A **run** records one execution, including its inputs, status, result, and trace. These distinctions separate revising a procedure, configuring where it operates, and examining what happened. The design addresses both authoring experience and execution behavior. Authors need to discover operations, connect their inputs and outputs, understand validation errors, and inspect results. The runtime needs corresponding rules for data contracts, control flow, state updates, and interruption. The thesis compares these concerns with other workflow systems and explains the prototype's choice to separate a node's data output from its routing outcome. The implementation provides Python authoring objects, a workflow service, and adapters for trusted Python functions and external tools. Evidence includes a deterministic case study and focused tests of validation, execution, and persistence. Authoring usability remains to be evaluated; the prototype's interaction design is still under development. # Introduction Preparing a weekly project report involves collecting notes, extracting actions and risks, checking the result, and producing a document that other people can use. A person can do this manually, write a script, or ask an AI assistant for help. When the procedure becomes recurring, the workspace also needs a way to preserve it, supply different inputs, and inspect unsuccessful attempts. `lda.chat` is a programmable workflow platform for that recurring work. It allows a human or external agent to assemble available operations into a saved procedure and execute it through a service. The procedure exists independently of the conversation or programming session that created it. Its executions can be inspected separately, and explicit requests for additional input can be resumed from saved state. For the report task, the initial procedure is short: Read the notes, extract a structured report, and render it as Markdown. The operator supplies new notes each week rather than rebuilding those steps. If the requirements change, the author revises the procedure and saves another version. If execution fails, the operator needs to identify the affected step and its inputs, not merely observe that no report appeared. ## From a useful procedure to a usable system The intended beneficiary is a workspace operator who needs repeatable work. Its author may be that person, a developer, or an external agent acting on their behalf. Authoring a procedure and operating a saved one are different activities; neither requires an LLM to participate in every execution. A workflow system must help its author answer practical questions: which operations are available, what information they require, and how one operation's result becomes another's input. During operation, it must distinguish an invalid definition, a missing service, a failed execution, and a request for more input. These are interaction-design concerns as well as runtime concerns. An interface that draws a branch without explaining whether one path or both will execute leaves a consequential rule implicit. The current prototype emphasizes programmable authoring and inspection. Its Python interface presents workflow and run objects instead of requiring authors to construct network messages. The interaction considered here is therefore code-based: discovering operations, revising a graph, and inspecting its executions through Python objects. ## Engineering question and contribution This thesis examines how authors can define a reusable procedure while the system manages its individual executions. Authors specify data connections and decisions about what happens next. The system must preserve those rules when steps repeat, when one workflow calls another, and when execution pauses for input. Separating these responsibilities allows a saved procedure to be executed without retaining the conversation or programming session that produced it. The prototype implements three design choices as one lifecycle: 1. A graph with declared input, output, and state schemas represents operations, data movement, and routing decisions so they can be validated and inspected. 2. Separate saved versions, environment bindings, and execution records make editing, configuration, and operation distinct activities. 3. Programmable authoring, validation diagnostics, and run inspection expose those distinctions to human and agent clients. The work integrates established workflow techniques into a service for reusable procedures. Its design is examined from both sides: what an author must understand and do, and what the runtime guarantees when the procedure executes. The comparison with related systems examines concrete authoring and execution mechanisms. ## Scope of the implementation The prototype supports conditional routes, iteration, child workflows, and explicit interruption/resume. Its runs have an execution-attempt limit. General parallel fork/gather remains proposed; persisted interruption does not mean recovery halfway through arbitrary external code. The evaluation separates tested behavior from unmeasured usability and production-readiness claims. ## Report Outline Chapter 2 derives interaction and execution requirements from the workspace task. Chapter 3 compares ways to author and operate that task in related systems. Chapter 4 explains the prototype's concepts through an example. Chapters 5 and 6 describe architecture and implementation; Chapter 7 presents the reproducible case study. Chapter 8 evaluates the available evidence. The remaining chapters discuss limitations, future work, and conclusions. # Problem Statement And Requirements A procedure that works once is not necessarily ready for repeated operation. For the report example, a renamed field may invalidate extraction, a different workspace may use another notes service, or the input may omit information needed by the final document. The author needs feedback that distinguishes these situations, while the runtime needs rules for handling them. LLM tool-use approaches illustrate dynamic selection of actions [@react-2022; @toolformer-2023]. Such approaches can incorporate schemas and persistence. This thesis examines which responsibilities belong in the saved procedure and which remain with its authoring environment. The requirements are organized into two groups. R1–R5 describe the authoring and operation experience the system should support. X1–X5 define the execution rules needed to support that experience. Their identifiers link the design to the evidence assessment in [@tbl:requirements-evidence]. ## Authoring and operation requirements The authoring requirements cover discovery, data movement, revision, version selection, and execution inspection: 1. **R1 — Discover before connecting.** Show available operations and the inputs, outputs, and services they require. The author should not need to inspect server implementation code to learn how an operation can be used. 2. **R2 — Make data movement understandable.** Explain how a document path becomes text, how text becomes structured fields, and which fields reach the result. Distinguish a data connection from a decision about what executes next. 3. **R3 — Support revision and useful feedback.** An author should be able to revise an unfinished procedure and locate errors in the relevant step or data mapping. Diagnostics should locate the rejected part and explain the violated constraint. 4. **R4 — Separate editing from running.** Make clear which version an execution uses. Editing the next version should not silently change an earlier one. 5. **R5 — Explain execution state.** Distinguish completed, failed, and interrupted runs, expose relevant intermediate evidence, and identify the input required to resume an interruption. Agent-assisted authoring places particular demands on this interaction. An agent must discover the available operations, interpret a rejected definition, and determine which changes are permitted without relying on implementation files. This motivates three complementary forms of support: descriptions of operations and their contracts, diagnostics that locate errors, and instructions for the authoring and execution lifecycle. Structured responses, stable identities, and inspection results with explicit size limits make that information available to programmatic clients. Human authors and other software clients use the same information to construct and operate workflows. A saved definition follows the same routing and data rules whether it was written by a developer or assembled by an agent. The evaluation therefore examines ease of use and compliance with execution rules as separate questions. ## Execution requirements behind the interaction Those interactions require corresponding runtime contracts (X1–X5): 1. **X1 — Preserve definitions and executions independently.** Save an identifiable procedure version and keep separate records for its invocations. 2. **X2 — Validate known constraints before work starts.** Check graph structure, declared data contracts, mappings, and required service bindings. 3. **X3 — Specify routing and state changes.** Define what selects the successor, where outputs are written, and how repeated writes affect the workflow's working data, referred to here as workflow state. 4. **X4 — Separate environment choices from procedure logic.** Resolve logical service requirements to concrete configured services and report mismatches. 5. **X5 — Bound and inspect execution.** Limit execution attempts, retain status and trace information, and support resume at defined interruption boundaries. Together, these requirements motivate separate records for saved procedures, environment selection, and individual executions. ## Costs and boundaries Schemas, bindings, and versioned definitions impose authoring work. A script can be preferable for a short-lived task, especially when its author already understands the libraries involved. The platform targets cases where reuse and inspection justify that setup; the thesis does not establish a numerical break-even point. A workflow interface must also avoid promising more than its runtime supports. Successful validation does not guarantee a remote service will succeed, and a saved interruption does not make arbitrary external effects reversible. # Positioning And Related Systems The report task provides a common lens for comparison: choose operations, pass notes between them, add a route for incomplete information, test the procedure, and inspect its result. The comparison follows these authoring activities into the execution rules they expose. The accounts below use documented mechanisms consulted in September 2026. They are not hands-on usability measurements or a complete product survey. Differences in authoring style do not establish that one system is easier for every audience. ## Starting with code or direct tool calls A developer can express the report procedure as function calls and use the language's conditionals and loops. An agent can instead select successive tool calls from the information available at each turn. Both approaches can be combined with schemas, tests, logs, and persistence. Scripts, including generated scripts, are a substantive alternative rather than merely a preliminary form of workflow automation. Code puts the procedure close to its implementation and makes ordinary debugging tools available. It also leaves decisions about configuration, saved versions, and run records to the program or its surrounding infrastructure. A workflow platform makes some of those decisions part of its public contract, at the cost of introducing another model for the author to learn. The prototype still uses Python for authoring. The distinction is whether that code performs the entire procedure directly or constructs a saved workflow for the service to execute. Neither representation is automatically better for a one-off task. For this comparison, state means working data retained during execution; a reducer defines how a new write changes a state field. The prototype's outcome is a routing label returned separately from a step's data output. Chapter 4 develops these concepts through the report example. The comparison asks the same questions of each system: what a step produces, how subsequent work is selected, how data combines, and how an author sees those rules. Suspension is considered separately from ordinary branching. ## n8n: connecting and inspecting data n8n passes arrays of data items between connected nodes. Items contain JSON data and may also contain binary data. Authors map fields from incoming items into node parameters; dragging a field into a parameter creates an expression. Thus, a connection participates in data flow as well as execution order [@n8n-data-2026]. For an author combining report records, n8n's Merge node exposes a concrete choice: append the incoming collections, match records by fields or position, or produce combinations. The node's configuration and worked examples make these operations distinguishable. Append waits for connected inputs and emits their items in input order [@n8n-merge-2026]. Selecting a merge mode changes which records appear in the output. For the report task, joining actions by owner is different from appending two action lists. The prototype represents data movement through explicit mappings. Its reducers define how writes update state fields. Joining records and waiting for branch completion are separate operations. Branching does not necessarily imply simultaneous execution. For workflows created from n8n 1.0, the documented default completes one branch before starting another, with ordering affected by canvas position and workflow settings [@n8n-order-2026]. The author therefore needs both item-level data inspection and an account of branch execution, not only a connected diagram. ## Zapier: configuring a decision Zapier exposes the output fields of earlier steps for mapping into later steps. Test records supply the values shown during configuration; live runs use their own data. This makes the mapping an explicit reference to a previous step, rather than an update to an author-declared shared state field [@zapier-mapping-2026]. An author using Zapier Paths selects fields, conditions, and values, then tests the rules against sample data. Applied to the report task, those rules could distinguish complete records from records needing attention. Multiple paths can qualify, so exclusivity must follow from the rules rather than the branching appearance alone [@zapier-paths-2026]. The same documentation describes sequential execution of qualifying paths. Paths does not provide a shared action after all branches; common steps can be duplicated or placed in a Sub-Zap. These constraints affect how the author organizes a common report-rendering step [@zapier-paths-2026]. The interface therefore needs to communicate both the condition being tested and the consequence of a match. In the prototype, an ordinary outcome selects one successor, whereas several Zapier Paths may qualify. ## LangGraph: describing decisions in Python LangGraph's Graph API lets an author define state, add node functions and routing, compile the graph, and invoke it. For the report task, a developer can represent extracted fields in state and write a routing function that selects what happens next [@langgraph-graph-api-2026]. This authoring style exposes more behavior as code. Nodes produce state updates, reducers determine how updates combine, and conditional routes select subsequent execution. The documented model supports graph loops and super-step execution; its checkpoint facilities are described separately [@langgraph-graph-api-2026; @langgraph-persistence-2026]. The prototype shares the use of typed state and reducers, but represents ordinary routing as a mapping from a declared outcome to a successor. This keeps executable predicates out of ordinary edges. The cost is that some decisions need a dedicated condition step or an additional declared outcome. The relevant comparison is where authors express and inspect decisions, not whether Python or a canvas is inherently the better interface. ## Saved Definitions, Configuration, and Execution Records Lifecycle separation is not unique to the prototype. n8n distinguishes saved edits from the published version used for production execution, and separates workflow history from execution history. Its execution view supports status filtering and inspection of previous attempts [@n8n-publish-2026; @n8n-executions-2026]. Zapier allows draft editing while a published Zap remains active. Publishing creates a version, and run details identify the version used and the data received and sent by individual steps. Connected application accounts are managed separately through app connections [@zapier-versions-2026; @zapier-history-2026; @zapier-connections-2026]. For LangGraph's library API, the graph is defined and compiled in application code. Checkpointers store execution snapshots organized by thread identity; the application can retrieve current state and state history. Application configuration and dependency provision remain part of the surrounding code. This library-level comparison does not cover hosted deployment products [@langgraph-graph-api-2026; @langgraph-persistence-2026]. The prototype exposes saved definitions, source selection, and run inspection as artifact, deployment, and run objects in its Python client. Its deployment mapping selects a configured provider that must satisfy the saved source requirements. An app connection or credential is therefore only a partial analogy: the source also supplies operations and their contracts. The design contribution is the composition of these established lifecycle responsibilities with the typed graph and client interface. Each system must distinguish an edit to future work from the recorded definition and data of a past execution. ## Implications for this design [@tbl:positioning-summary] compares the documented mechanisms with the prototype's ordinary execution model. Its rows describe selected mechanisms, not every extension available in each product. The sources for n8n, Zapier, and LangGraph are discussed in the preceding subsections. | System | Data | Control selection | Combination | | --- | --- | --- | --- | | n8n | Item arrays | Branch connections | Merge modes | | Zapier | Prior-step fields | Matching Paths | Explicit later actions | | LangGraph | State updates | Edges and routers | Field reducers | | Prototype | Output-to-state mappings | One outcome edge | Field reducers | : Data and control mechanisms compared. {#tbl:positioning-summary} For the report task, these models put different work on the author. n8n requires attention to which items reach each node; Zapier requires mappings from earlier steps and rules for qualifying paths; LangGraph requires state and routing code. The prototype instead requires explicit output-to-state mappings and declared routing outcomes. These differences concern where the procedure's meaning is expressed, not just whether its editor is visual. Pausing also has a separate contract. n8n's Wait node can resume on a time, webhook, or form condition [@n8n-wait-2026]. LangGraph's dynamic interrupt uses a checkpoint and thread identity; resuming restarts the interrupted node, so code preceding the interrupt executes again [@langgraph-interrupts-2026]. The prototype uses an explicit interruption boundary and a declared resume payload. A failed operation is not automatically such a pause: for example, Zapier documents that an errored step produces no output fields for subsequent mappings [@zapier-mapping-2026]. These observations do not establish equivalent retry or side-effect guarantees across the systems. The prototype prioritizes programmable authoring, explicit mappings, and saved execution records. Its current interface must still be evaluated for the work required to discover operations, repair definitions, and interpret results. Neither its typed models nor the absence of executable edge predicates proves that it achieves those user-experience goals. External-tool protocols are a separate concern. Model Context Protocol (MCP) exposes tools, resources, and prompts; it is not a competing graph model [@mcp-tools-2025; @mcp-lifecycle-2025]. In this system, a source is a configured provider of operations. MCP is one way to obtain those operations; it does not determine the workflow's routing or data model. # Conceptual Model The report example introduces the concepts in the order an author encounters them: choose operations, connect their data, define decisions, save a version, and inspect an execution. The branching example in [@fig:report-branch] explains supported primitives; it is not an additional measured case study. ## Operations, data, and decisions Suppose report preparation must ask for missing information before rendering: ```{.mermaid #fig:report-branch width=95% caption="Alternative routes to report rendering."} %%{init: {"flowchart": {"rankSpacing": 20, "nodeSpacing": 20}}}%% flowchart LR Read["Read
notes"] --> Extract["Extract
report"] Extract --> Check{Complete?} Check -->|ready| Render["Render
report"] Check -->|needs_information| Ask["Request
information"] Ask -->|submitted| Render Render --> Finish([End]) ``` Each named operation is a **node** in the workflow. An **edge** selects the next step after a node produces an **outcome**, such as `ready` or `needs_information`. The node's **output** is the data it returns, such as the extracted report fields. A missing-information outcome is a business decision; an exception while reading a file is a runtime failure. The arrow to the next step does not implicitly pass all previous output into that step. The author defines input mappings and writes relevant outputs into workflow state. In this example, extraction writes report fields, the request can supply missing fields on resume, and rendering reads the resulting report. Only one of the two routes to rendering executes on each decision. **Schemas** declare the shapes of accepted inputs and produced results. They help the author see which fields an operation requires and allow the validator to detect incompatible mappings. They do not establish that an extracted fact is true or that a remote operation will succeed. A node output is the data returned by one step. The workflow output is the public data selected when the procedure completes. A run records that workflow output alongside status, diagnostics, execution identity, and trace information. ## Workflow state and iteration **State** is the workflow's working data. A **reducer** specifies how a write changes a state field: replace the previous value, append an action item, or add a number, for example. Reading a field and routing to another step are separate operations. This makes data movement inspectable but requires the author to understand the mappings. If the procedure processes several documents, a foreach step defines an item body. The foreach distinguishes completion of one item from completion of the collection. A child workflow provides a separate invocation scope with explicit inputs and results. These boundaries define which data is available and what completion means. Nodes outside an iteration body, or within the same body, may form ordinary cycles. A body and its enclosing workflow are separate control regions; an ordinary edge cannot cross that boundary. A run-wide step budget limits execution attempts. General fork/gather is not implemented. Connecting paths does not by itself promise parallel execution, synchronization, or conflict-free state merging. ## Saving, configuring, and running the procedure While authoring, an **editable workflow** is the mutable definition being revised. Saving creates an **artifact**: an immutable workflow version containing its graph and declared requirements. A **deployment** selects a saved version and connects its logical service requirements to concrete configured services. For example, the same report procedure could use a test notes source in one deployment and a production notes source in another, provided both satisfy its required contracts. Changing the bindings is different from changing how the report is assembled. A **run** records one execution of a deployment. Last week's successful report and this week's interrupted attempt are different runs, even if both use the same artifact and deployment. Inspecting a run should identify the version, inputs, status, and available execution evidence without changing the saved procedure. [@fig:lifecycle-records] relates the saved version, its deployments, and their runs. The separate records preserve the distinction between changing a procedure and examining an execution of it. ```{.mermaid #fig:lifecycle-records width=95% caption="One saved version can serve several deployments and runs."} classDiagram direction LR class EditableWorkflow { mutable graph } class ArtifactVersion { artifact_id version saved definition } class Deployment { deployment_id source bindings } class Run { run_id input and result status and trace } EditableWorkflow ..> ArtifactVersion : saves ArtifactVersion "1" <-- "0..*" Deployment : selects Deployment "1" <-- "0..*" Run : started from ``` ## Available operations and environment binding A **capability** is an operation available for use in a workflow. A **source** groups capabilities under a configured identity. The report's extraction operation might come from trusted Python code, while another operation is supplied by an external tool service. A **binding** is an explicit mapping. Input and output bindings map data; a deployment binding connects a logical source requirement in the workflow to a concrete source in the environment. Validation checks whether that source exists and matches the saved requirements. **Source drift** means those requirements no longer match the currently available capabilities, for example after an input schema changes. Built-in sources have fixed platform identities and do not require those deployment bindings. Configured sources remain explicit operator choices. Portability is limited to environments with compatible code, credentials, and services. ## Inspecting failure and resuming an interruption A validation diagnostic concerns the definition or its dependencies before execution. A failed run records an operational problem encountered during execution. An interrupted run records an explicit request for input, together with the state needed to continue. The interface should distinguish these situations because they call for different actions. In the report example, `needs_information` routes to a request step. The run then waits for a declared resume payload; resuming applies that payload through the workflow's bindings and continues to rendering. This is a defined pause in the procedure, not recovery halfway through an arbitrary handler. A **trace** records execution evidence associated with the run. It supports questions such as which route was taken and where execution stopped, but does not make external effects reversible. The implementation chapters explain how these concepts become runtime records and service operations. ## Working Glossary [@tbl:working-glossary] summarizes the core terms through the report example. | Term | Meaning in the report example | | --- | --- | | Capability | An available operation, such as extracting report fields | | Node | One use of an operation or control step in the procedure | | Output | Data produced by a step | | Outcome | A declared label selecting the next step | | State | Working data retained during execution | | Artifact | An immutable saved version of the procedure | | Deployment | A saved version connected to concrete services | | Run | One execution with its own status and evidence | | Source | A configured collection of available operations | | Binding | An explicit data mapping or source-to-environment mapping | : Working glossary for the thesis terminology. {#tbl:working-glossary} Python types, provider protocols, and package boundaries implement these concepts. Their names are introduced with their responsibilities in the architecture and implementation chapters rather than used as prerequisites for understanding the workflow. # System Architecture The weekly-report example needs more than a graph executor. An author must discover the available operations, connect them, check the resulting workflow, and choose which saved version to run. An operator must then distinguish a bad definition from an unavailable service or an interrupted execution. The architecture separates these responsibilities without requiring each caller to implement the workflow lifecycle. Three boundaries organize the system: authoring versus server operations, saved definitions versus individual executions, and workflow execution versus provider-specific calls. These boundaries are visible in the user-facing objects as well as in the implementation. ## From Authoring to Server Operations The Python client is the main programmatic authoring interface. Its `App` object represents a connection to the workflow service. An author can inspect a capability, use the returned object in an editable workflow, validate that workflow, and save it. The client reconstructs server responses as Python objects with relevant operations, rather than requiring application code to carry response dictionaries through every step. Local editing does not require a server request for each graph change. Discovery and persistence do: the service owns the available capability inventory and stored records. Validation therefore has both a local part, which checks the authored structure, and a server part, which checks it against the service's contracts. [@fig:architecture-spine] shows this separation. The CLI is another entry point to the service; it is not a mandatory layer beneath Python authoring. ```{.mermaid #fig:architecture-spine caption="Clients share lifecycle services and provider-independent execution."} flowchart TB subgraph Authoring["Authoring and clients"] Python["Python App and editable workflow"] Client["Client port"] CLI["Command-line interface"] Python --> Client end subgraph Service["Server-composed services"] API["Workflow API"] Stores["Artifacts, deployments and run records"] Inventory["Available operation contracts"] end subgraph Transport["Transport boundary"] RPC["HTTP JSON-RPC adapter"] end subgraph Execution["Workflow execution"] Core["Workflow execution core"] end subgraph Integrations["Provider integrations"] Providers["Configured source providers"] Handlers["Execution handlers"] end CLI --> RPC Client --> RPC RPC --> API API --> Stores API --> Core Inventory --> API Providers --> Inventory Core --> Handlers Providers --> Handlers ``` The API operation layer is independent of the wire protocol. The JSON-RPC adapter translates requests and responses; it does not decide how a foreach iteration returns or how a reducer applies a state update. Conversely, the execution core does not need to know whether its caller used Python, a command line, or another application. This distinction also limits the role of an agent. An agent can help author or operate a workflow through these interfaces, but the runtime follows the saved graph. It does not ask an agent to choose the next step unless the author has explicitly included an operation that makes such a decision. ## Keeping a Definition Separate from Its Use The client owns the editable graph; the service owns saved artifact, deployment, and run records. The API coordinates validation and persistence through their respective stores. Execution reads a selected definition and its resolved environment rather than the client's mutable editor contents. For the report workflow, this boundary prevents an unfinished edit to the extraction step from becoming the definition of an already-created run. Inspection retrieves the run's recorded identity and execution evidence; it does not reconstruct the attempt from whatever is currently open in the author's session. The client exposes this progression through workflow artifact, deployment, and run objects. A run object contains a snapshot of the stored execution. Refreshing it requests a new snapshot. This makes network activity explicit, although applications must decide when to refresh and how to present progress. Draft workspaces provide a separate persisted editing surface used by the draft API and CLI. They are not a required intermediate object for every Python authoring operation. Both routes ultimately produce a saved definition that the deployment and execution layers can use. ## Data Movement and Control Movement Within a workflow, input bindings supply a step's arguments. Output bindings select returned values to write into workflow state. Reducers determine how those writes combine with existing values. The returned outcome selects the next edge. These are related operations, but none substitutes for another: routing to a renderer does not, by itself, supply the report it needs. [@fig:node-execution-cycle] summarizes an ordinary callable step. Conditions, iteration controllers, subgraphs, and interrupts have their own runtime handlers rather than pretending to be remote capability calls. ```{.mermaid #fig:node-execution-cycle caption="A step updates state before following its selected route."} flowchart TB Validation["Input validation"] --> Call["Invoke operation"] Call --> Result["Checked result"] subgraph Data["DATA: what becomes visible"] Output["Output payload"] --> Bind["Output bindings"] Bind --> Reduce["Reducers update state"] end subgraph Control["CONTROL: where execution goes"] Outcome["Declared outcome"] --> Route["Select matching edge"] Route --> Next["Next node"] end Result --> Output Result --> Outcome Reduce -.->|state available to next step| Next ``` A declared outcome such as `needs_information` is a workflow decision. A handler exception or exhausted step budget is an execution failure. A workflow can therefore complete with a non-success business outcome without being a failed runtime execution. Where iteration supports collecting item errors, that policy must be explicit; errors do not automatically become ordinary outcome edges. The runtime performs the illustrated operations in sequence: it applies the result's writes before advancing along the selected route. The trace makes the sequence inspectable, but a fixed graph does not imply identical external results. Language-model calls, remote services, and concurrent completion order can vary between runs. The runtime's defined routing and state-update rules should not be confused with reproducibility of every operation it invokes. ## Iteration and Child Workflow Boundaries Suppose the report now covers two documents, `A.md` and `B.md`. The workflow must render a report for each, then assemble the two reports. For this first example, foreach is configured to process one document at a time. An output binding appends each rendered report to a `reports` state field; the assembly step reads that field. The important distinction is between finishing **one document** and finishing **the whole collection**. After A finishes, the foreach continues with B. Only after B finishes does it follow its `done` route to assembly. It does not begin the collection again whenever an item returns. [@fig:foreach-region] follows one invocation from start to finish. Read downward for time. Solid arrows request work or apply a data binding; dashed arrows report completion. The labels identify the data and routes. ```{.mermaid #fig:foreach-region width=95% caption="Serial iteration finishes both documents before assembly."} sequenceDiagram participant Each as Foreach participant Body as Render item participant State as reports state participant Assemble as Assemble Note over Each,State: Start once: documents = [A.md, B.md], reports = [] Each->>Body: loop: process A.md Body->>State: Output binding: append reportA Body-->>Each: ok: A is finished Each->>Body: loop: process B.md Body->>State: Output binding: append reportB Body-->>Each: ok: B is finished Note over Each: Both items finished Each->>Assemble: done: continue after the loop Assemble->>State: Input binding: read reports State-->>Assemble: [reportA, reportB] ``` The same authored render node executes twice. The runtime distinguishes those executions so that finishing A advances to B, while finishing B permits assembly. Each item returns to the foreach invocation that started it. This explains the graph's back-edge: `render.ok -> each` is an item return, whereas `each.done -> assemble` leaves the loop. The output binding is what appends the report; the back-edge itself does not transport or collect data. The graph validator rejects a body node used both inside and outside that loop, or a nested body that returns past its immediate owner. ### Calling a Child Workflow for One Document Now replace the rendering work with a call to a saved child workflow that summarizes one document. Consider only the item for `A.md`. The parent may know the whole collection, but the child receives only the input explicitly mapped into its call: `{"document": "A.md"}`. The child has its own input, working state, and execution context. It cannot read the parent's current item or `reports` field merely because the parent called it. If it needs another value, the author must add an input binding. [@fig:scope-boundaries] shows the call and its return. The item waits while the child runs. The child's `END` completes that child invocation, not the parent item or the whole report workflow. ```{.mermaid #fig:scope-boundaries width=95% caption="Child completion precedes the parent item's return to foreach."} sequenceDiagram participant Item as Parent item A participant Child as Child workflow participant Result as Item A reports writes participant Each as Foreach Item->>Child: Input binding: document = A.md activate Child Note over Item: Wait for child Note over Child: Own input,
state and context Child->>Child: Produce summaryA Child-->>Item: END: return summaryA deactivate Child Item->>Result: Output binding: append summaryA to reports Item-->>Each: ok: item A is finished Note over Each: Continue according
to foreach mode ``` Input and output bindings cross the child workflow boundary explicitly. The two completion points are separate: child `END` returns to the calling node, and the calling node's route back to foreach finishes the item. ### When Items Run Concurrently In concurrent mode, A and B may be in progress together. A returning from its child does not permit assembly while B is still running. The foreach waits for its required item completions before following `done`. Concurrent items retain separate pending writes until their results combine. In [@fig:scope-boundaries], “Item A reports writes” represents these pending writes. In serial mode, the output binding updates enclosing state so the next item can read it. For the successful two-item concurrent case, the foreach combines A's and B's writes using the declared reducers, then assembly reads the combined state. An append reducer and a replace reducer have different effects; neither the arrows nor the fact that both items completed chooses a merge policy. The serial ordering in [@fig:foreach-region] is not a promise about concurrent completion order. If a child or item reaches an explicit interrupt, the run must preserve which document was active and where it was waiting. Resume continues that saved work; it does not restart the collection or silently move the response to another item. ## Connecting External Operations Providers adapt external functionality to contracts the workflow system can inspect and invoke. Discovery supplies schemas and declared outcomes; execution supplies a handler for the selected binding. These are separate responsibilities because discovering an operation does not mean its service is currently reachable or authorized. The server composes providers, stores, and the API. MCP-backed operations may need sessions, authentication, and remote catalog handling. Python-backed operations use configured, trusted imports. These concerns remain outside the graph scheduler. An experimental OpenAPI provider explores another source family without making it the system's product identity or implying that all API descriptions are interchangeable. This boundary reduces provider-specific logic in workflows, but cannot erase provider differences. A schema describes a call's shape; it does not guarantee availability, cost, side-effect safety, or semantic equivalence to another operation with the same fields. # Implementation The Python package structure localizes changes to the relevant responsibility. Improving a Python editing method should not require changing the scheduler, and adapting a new remote service should not require changing graph routing. ## Following a Python Authoring Request The client package, `wf_client`, connects Python objects to a narrow `WorkflowClientPort`. `App.from_http_jsonrpc(...)` configures the connection without making a request. A subsequent capability inspection performs I/O, decodes the response, checks the returned identity, and constructs a `RemoteCapability`. An `EditableWorkflow` subclasses the authoring layer's `WorkflowBuilder`, reusing its graph-building methods. It adds remote validation and saving rather than maintaining a second independent builder implementation. Its validation method contains this early return: ```python local = self.validate_local() if not local.ok: return WorkflowValidation(local, "not_run", ()) ``` This excerpt from `wf_client/authoring.py` explains an observable behavior: a structurally invalid edit produces local feedback without a server request. Passing that check does not establish deployment readiness; the server still validates the submitted plan against its inventory. Saving validates first, submits the plan, checks the save response, and re-inspects the exact artifact version. The resulting artifact object is therefore reconstructed from the stored definition. Identity checks reject responses identifying a different artifact or deployment. The same principle applies to runs. In `wf_client/runs.py`, `refresh()` returns a decoded snapshot from `inspect_run`, checking the expected run and deployment identities. `resume()` returns another snapshot after submitting the response. Application code must retain the returned object; an earlier snapshot does not mutate when the server advances. Serialization still exists at the service boundary. The benefit is that callers need not manually rebuild the domain objects after every request. The client does not eliminate the distinction between a local Python model and a remote operation. ## Executing and Bounding a Run The `wf_core` package defines the graph and execution state. Its runtime selects ready frames and dispatches by node kind. An ordinary `NodeUse` invokes a bound callable; `ConditionNode`, `ForeachNode`, `SubgraphNode`, `InterruptNode`, and `EndNode` implement explicit control behavior. Step admission happens before dispatch. The immutable `RunLimits` policy sets a positive maximum, with a default of 10,000 attempts. The run stores how many attempts have been admitted. Nested execution shares that run-wide budget, and asynchronous dispatch reserves attempts before launching work. Failed step attempts also consume the budget. The budget bounds graph progress, including a cycle whose condition never selects an exit. It is not a wall-clock timeout, a language-model token allowance, or protection against a handler that blocks indefinitely. The limit and consumed count persist with the run, so interrupting and resuming does not reset the allowance. ## Preserving State Across Nested Execution The runtime represents an individual execution position as a **frame**. An item frame identifies its owning foreach invocation, distinguishing a return from an item from a new entry into the controller. A **scope** contains the input, state, and context of one workflow invocation. Child workflows receive their own scopes. A **lineage** records a separate state history; its buffered writes keep concurrent items isolated until merging. Workflow state updates pass through reducer-aware patches. Serial iteration must make its writes visible to subsequent serial work, while concurrent items need separate views until their results are combined. Nesting either mode inside the other makes write ownership more subtle than committing every result directly to global state. Two rules govern this routing: serial work observes preceding serial writes, and concurrent siblings do not observe each other's unmerged writes. The shared `commit_foreach_aware_patch` helper handles writes from ordinary nodes, subgraph results, and interrupt responses. It walks serial owners outward and selects the first concurrent item boundary, if present, as the buffer destination. It continues checking ancestry before writing, so a missing parent or cycle cannot cause a partial write merely because a buffer destination was already found. With no concurrent boundary, the patch commits through the enclosing serial owners. Combining results must also preserve each write's contribution, rather than count a previously merged value as new work. For example, if an enclosing list already contains `A` and an inner group appends `B` and `C`, its visible result is `[A, B, C]`, but its contribution is only `[B, C]`. Appending the visible result again would duplicate `A`. When concurrent results are combined, the patch retains their constituent write contributions for later reducer replay. Keeping only cumulative values would allow a surrounding iteration to replay an already-counted prefix. This distinction matters for operations such as appending report sections: a correct visible value at one nesting level is not necessarily a correct contribution to the next merge. This implementation supports iteration; general fork/gather remains future work. ## Giving Expressions a Consistent Context Bindings and conditions need the same account of the current execution. The runtime's `frame_context_view` derives structured foreach entries from persisted frame ancestry. Entries are keyed by foreach node identity, so nested bodies can refer to enclosing items within the same workflow scope, as well as the current item. The walk stops at a subgraph scope boundary. An enclosing item's value must be passed as child input if the child needs it. The reader also rejects malformed ownership, parent cycles, and conflicting aliases; corrupt checkpoint metadata is not treated as an innocently absent field. A context path must have the same meaning in a condition and an input binding within one execution. Both receive the structured mapping, while validation checks paths against the corresponding context schema. Otherwise, a condition testing whether the current item exists could select the false route even though an input binding can read that item. ## Validation, Persistence, and Diagnostics The authoring layer checks graph structure; the server checks saved plans and environment bindings; the runtime checks actual values and execution state. Each layer has information the earlier one lacks. Static validation can reject an invalid foreach return, for example, but cannot prove that a remote operation will remain available when a run reaches it. Diagnostics carry a code, a location, a message, and, where available, a repair hint. These fields let a caller identify the faulty binding or node without parsing a prose-only error. Suggested next actions are guidance, not authorization and not evidence that a repair has succeeded. Schema fingerprints likewise detect a changed contract representation; they do not prove semantic compatibility. The API lifecycle layer persists stopped runs and their versioned checkpoints, including interrupted runs that may later resume. Restoration validates the stored representation and recovers the execution state before dispatch continues. This supports explicit pause-and-resume boundaries; it does not promise durable recovery from every instruction inside an arbitrary handler or exactly-once external side effects. Separating artifact, deployment, and run storage also keeps inspection focused. Definition inspection explains what was saved; deployment inspection explains environment selection; run inspection and trace explain what happened during a particular attempt. ## Server and Provider Responsibilities The remaining package boundaries put these operations into a service. `wf_api` coordinates lifecycle operations, `wf_artifacts` supplies storage contracts and implementations, and `wf_platform` supplies shared platform contracts. `wf_server` composes these dependencies. `wf_transport_rpc_http` exposes the JSON-RPC interface, while `wf_cli` provides terminal operations. Provider implementations retain their own lifecycle requirements. MCP support manages remote discovery and invocation through configured connections. Python support loads trusted configured callables; it is not a sandbox for arbitrary submitted code. OpenAPI support remains experimental and is not evidence that every described HTTP service can already be used without adaptation. The case study follows these components through one public authoring session: discovery, revision, validation, persistence, execution, and inspection. # Case Study: Deterministic Report Workflow Consider an author who receives weekly notes and needs two deliverables: a structured report for further processing and Markdown for a reader. The example workflow makes this small procedure reusable: The three operations read notes, extract a report, and render Markdown. The input is deliberately constrained. Notes contain named sections and action lines with owner, task, and due-date fields. Extraction parses that format; it is not language-model summarization of arbitrary documents. With fixed input and local Python operations, the result can be checked without remote credentials, service quotas, or variation in generated text. The bundle at [`examples/report_workflow/`](../../examples/report_workflow/) supplies the operations, fixture notes, server configuration, and a saved raw-plan example. The walkthrough below expresses the same procedure through the current Python client. The example README retains a command-line route for operators who need it; that route is not a prerequisite for using this interface. ## Starting with Available Operations The operator supplies the source identities through the example configuration. This walkthrough discovers operations within those known sources; source administration is outside the session. The example configuration registers three trusted Python operations under `local.report` for discovery and `local.report_runtime` for execution. Their Pydantic models describe the input and output contracts. The author consumes those operations from the service inventory; the client does not import their implementations to execute them locally. The following blocks form one asynchronous Python session. The retained test executes these blocks against the real service in process, replacing only the HTTP connection assignment. It does not verify remote server startup. For HTTP use, the blocks assume a server using `examples/report_workflow/wf.config.json`, reachable at its configured address, and a fresh artifact name or unused version. The fixture read assumes the client is running from the repository root. ```python from pathlib import Path from pydantic import BaseModel from wf_authoring import input_from, input_path, output_to, state_path from wf_client import App app = App.from_http_jsonrpc("http://127.0.0.1:8771/rpc") available = await app.capabilities(source_id="local.report") for operation in available.items: print(operation.qualified_name) read_notes = await app.capability("local.report.read_notes") extract_report = await app.capability("local.report.extract_report") render_report = await app.capability("local.report.render_markdown_report") print(extract_report.output_schema) ``` Each lookup returns a capability object with its schemas and declared outcomes. The extraction schema exposes `title`, `summary`, `action_items`, `risks`, and `followups`; each action item has `owner`, `task`, and `due` fields. The author can use the schema directly or declare corresponding local models. This session uses local models without importing provider implementation code. The listing contains `local.report.read_notes`, `local.report.extract_report`, and `local.report.render_markdown_report`. It establishes the available names; the inspected contracts then describe how each can be connected. The author still selects the operations, rather than the service synthesizing a plan. Before connecting operations, the author checks their input and output contracts for compatible fields. ## Describing the Workflow's Data The workflow has one public input, intermediate state, and two public outputs. They are declared separately so that intermediate notes do not accidentally become part of the result contract. ```python class ActionItem(BaseModel): owner: str task: str due: str class ReportOutput(BaseModel): title: str summary: str action_items: list[ActionItem] risks: list[str] followups: list[str] ``` These author-defined models represent the discovered report fields. The workflow then declares its own input, working state, and public output: ```python class NotesInput(BaseModel): text: str class ReportState(BaseModel): notes: str = "" report: ReportOutput | None = None markdown: str = "" class ReportResult(BaseModel): report: ReportOutput markdown: str graph = app.new_workflow( "report_python_showcase", input_schema=NotesInput, state_schema=ReportState, output_schema=ReportResult, ) ``` The models export schemas for the workflow contract. They do not make the saved workflow dependent on a live Python class instance. The initially absent report belongs to intermediate state; the public result requires a report because a completed successful pipeline should have produced one. ## Connecting Data and Decisions The author now creates three node uses. Each use selects an operation and declares its data bindings: ```python read = graph.use( read_notes, id="read", input=[input_from(input_path("text"), "text")], output=[output_to("text", state_path("notes"))], ) extract = graph.use( extract_report, id="extract", input=[input_from(state_path("notes"), "text")], output=[output_to((), state_path("report"))], ) render = graph.use( render_report, id="render", input=[input_from(state_path("report"), "report")], output=[output_to("markdown", state_path("markdown"))], ) end = graph.end("ok", id="finished") graph.set_entry_point(read) graph.connect(read, "ok", extract) graph.connect(extract, "ok", render) graph.connect(render, "ok", end) graph.set_output([ input_from(state_path("report"), "report"), input_from(state_path("markdown"), "markdown"), ]) ``` The empty tuple in `output_to((), ...)` selects the extraction step's whole output object. The other output bindings select individual fields. The `connect` calls then specify execution order for the `ok` outcome; they do not implicitly carry those objects between steps. The author declares data mappings and execution routes separately. Changing one leaves the other unchanged. This requires additional declarations even for a linear three-step procedure; typed helpers construct the serialized representation from those declarations. ## Diagnosing and Repairing a Binding An editable graph can temporarily be invalid. Suppose a final output binding names a state field that does not exist: ```python graph.set_output([ input_from(state_path("missing_report"), "report"), input_from(state_path("markdown"), "markdown"), ]) broken = await graph.validate() assert not broken.ok assert broken.remote_status == "not_run" for issue in broken.local.errors: print(issue.code, issue.path, issue.message) graph.set_output([ input_from(state_path("report"), "report"), input_from(state_path("markdown"), "markdown"), ]) (await graph.validate()).raise_for_errors() ``` The diagnostic has code `invalid_source_path` and location `output[0].path`. Its message is: > source path must start with input., state., or context. and reference a > declared root field when applicable This locates the rejected mapping but does not name a replacement field; the author must compare it with the declared state. No server validation request is made for that invalid graph. The repair changes the workflow's output mapping. The renderer and its outgoing edge remain unchanged. This illustrates why data bindings and control routes need separate feedback. The next section saves only the repaired definition. ## Saving a Version and Choosing Its Environment Before saving, the author can request validation and inspect its diagnostics. The example stops on errors: ```python validation = await graph.validate() validation.raise_for_errors() artifact = await graph.save(version=1) deployment = await artifact.deploy( "report_python_showcase.local", bindings={"local.report": "local.report_runtime"}, ) readiness = await deployment.validate() if not readiness.runnable: raise RuntimeError(readiness.diagnostics) ``` The artifact is the saved version of the authored procedure. The deployment maps the saved `local.report` requirement to `local.report_runtime`. The fixture registers compatible operations under both names so this choice changes the execution source without changing the graph or requiring remote credentials. It demonstrates rebinding, not migration between real services. [@fig:python-lifecycle] summarizes the public operations; it omits internal validation and re-inspection calls made by individual client methods. ```{.mermaid #fig:python-lifecycle caption="Local editing leads to saved, configured, and inspected execution."} sequenceDiagram actor Author participant Client as Python client participant API as Workflow service participant Provider as Bound operation Author->>Client: Build and revise graph Client->>Client: Check structure locally Client->>API: Validate and save definition API-->>Client: Saved artifact version Client->>API: Bind deployment to version API-->>Client: Deployment and readiness Client->>API: Run with input API->>Provider: Invoke graph steps Provider-->>API: Outputs and outcomes API-->>Client: Stopped run snapshot Client->>API: Inspect run and bounded trace API-->>Client: Stored execution evidence Client-->>Author: Result or diagnostic ``` An edit to the workflow would be saved as another version, not applied retroactively to the artifact used by this deployment. Conversely, selecting a different source environment is a deployment concern. A readiness check can reject a missing or incompatible binding before a run is attempted. ## Running and Inspecting the Result The client reads the notes and sends their contents. The server therefore does not need access to the client's file path. ```python notes = Path("examples/report_workflow/input.md").read_text(encoding="utf-8") run = await deployment.run({"text": notes}, max_steps=100) if run.status != "completed": raise RuntimeError((run.status, run.diagnostics)) result = ReportResult.model_validate(run.output) assert result.report.title == "Weekly Project Update" assert len(result.report.action_items) == 3 assert result.markdown.startswith("# Weekly Project Update") run = await run.refresh() trace = await run.trace(start=0, limit=10) ``` The run exposes a status, output, and diagnostics independently of the editable graph. The output crosses the API as data; the explicit `model_validate` call reconstructs the example's Pydantic result model. Refreshing obtains the latest stored snapshot, while the bounded trace provides step-level evidence when output alone is insufficient. For this fixed fixture, the result includes three action items, the recorded risks and followups, and a Markdown report headed “Weekly Project Update.” Checking these fields establishes that the example's data reached the intended outputs. It does not establish that the report is useful for every reader or that extraction works on unstructured notes. ## What This Case Demonstrates The executable check in [`test_thesis_python_walkthrough.py`](../../tests/examples/test_thesis_python_walkthrough.py) reads and runs this chapter's Python blocks, including the rejected binding and its repair. It checks the stored report as well as the returned snapshot. The existing tests in [`test_report_workflow_example.py`](../../tests/examples/test_report_workflow_example.py) check the source's input rules, rendering and extraction, capability discovery and invocation, and the artifact/deployment/run lifecycle using the raw-plan fixture. Those tests are evidence for the report operations and lifecycle. They complement the Python session check; neither is a user study. The Python presentation makes the current authoring experience concrete: inspect operations, declare contracts, connect data and outcomes, save, select bindings, and inspect an execution. The CLI and draft surface offer another way to perform related lifecycle operations; they are not required steps in this Python walkthrough. A direct Python script would be shorter for these three local functions. The additional structure becomes relevant when the definition must be saved, bound to an environment, validated independently, and inspected through a shared service. This example demonstrates that integration, not a performance advantage over function calls. Nor does a linear pipeline exercise all graph semantics. It has no conditional branch, foreach body, child workflow, or interrupt. Targeted runtime tests provide evidence for those mechanisms; they should not be credited to a case that never executes them. Ease of authoring and diagnosis also requires evidence beyond a successful fixture run. # Evaluation Evaluation distinguishes three questions: whether the runtime follows its contract, whether the public lifecycle composes correctly, and whether an author can use that lifecycle effectively. The current evidence addresses the first two through controlled tests and an adapted in-process walkthrough. The authoring assessment identifies the operations available through the interface. Usability remains a separate evaluation question. ## Requirements and Evidence [@tbl:requirements-evidence] relates the requirements to the available evidence. The walkthrough demonstrates interface operations supporting R1–R5. The repository also contains tests covering X1–X5. The table distinguishes these sources from the usability measurements still needed. | Requirement | Evidence | Assessment | | --- | --- | --- | | R1 Discovery | Walkthrough; E2 | Exercised; usability unmeasured | | R2 Data movement | Worked graph | Explained; comprehension unmeasured | | R3 Revision feedback | Binding repair; E3 | Exercised; usability unmeasured | | R4 Editing vs running | Lifecycle; E1–E2 | One saved version exercised | | R5 Status interpretation | Inspection; E1–E2 | Exposed; usability unmeasured | | X1 Definition/run identity | Lifecycle; E1 | Lifecycle tests listed | | X2 Known constraints | Validation; E3 | Constraint tests listed | | X3 Routing and state | Runtime; E4 | Runtime tests listed | | X4 Environment choices | Sources; E5 | Provider tests listed | | X5 Bounds and inspection | Budget/resume; E1, E4 | Boundary tests listed | : Requirements and available evidence. {#tbl:requirements-evidence} The R4 observation covers saving and running version 1. The session does not edit a later version or check its effect on earlier runs. Understanding the version distinction remains part of the authoring evaluation. X5 contains three distinct obligations: admission limits, execution inspection, and interruption resume. Budget tests exercise limit persistence and exhaustion; lifecycle tests exercise stored inspection and resume. The report session exercises inspection but does not exhaust its budget or interrupt. The evidence index locates these separate checks; it is not a record of a newly executed full-system suite. ## Walkthrough Method and Observations The retained walkthrough test uses the example server configuration and a fresh pytest temporary store. It extracts the case study's Python blocks in document order and substitutes an in-process client connection for the HTTP connection assignment. All later calls use the real API, provider, and stores. This setup exercises discovery, graph construction, validation, saving, deployment, execution, refresh, and trace inspection, while excluding network startup and transport behavior from the observation. [@tbl:prototype-conformance] records the concrete checks performed by that session. The fixture expectations are fixed independently of the workflow's result, and stored output is inspected again through the API. | Check | Expected observation | Requirement | | --- | --- | --- | | Invalid output mapping | Local rejection at `output[0].path` | R3, X2 | | Repaired graph | Validation permits saving and running | R3, X2 | | Persisted report | Expected title and three action items | X1, X3 | | Persisted rendering | Expected Markdown heading | X3 | | Run and trace | Completed snapshot; recorded steps | R5, X5 inspection | : Reproducible report-session checks. {#tbl:prototype-conformance} The checked session completed with the expected title, three action items, and Markdown heading in the saved output. The invalid binding was rejected locally; restoring the mapping allowed validation and execution to continue. These observations establish the displayed procedure's behavior for this fixture, not an author's ability to construct it unaided. The session check and the existing report-example tests can be run with: ```powershell $suites = @( "tests/examples/test_thesis_python_walkthrough.py", "tests/examples/test_report_workflow_example.py" ) uv run pytest @suites -q -n 0 ``` The first suite executes the displayed case-study session with the stated transport substitution. The second checks the operation fixtures and raw-plan lifecycle independently. Document rendering and generated PDF assets are not part of this command. The test and manuscript must be taken from the same repository revision because the test reads the manuscript directly. ## Separating Design Comparison from Evaluation The earlier comparison of n8n, Zapier, and LangGraph explains different authoring and execution choices. The same task has not been measured across those systems under matched conditions. This report therefore cannot rank their usability, reliability, or performance against the prototype. Execution tests ask whether contracts, routing, state updates, and persistence behave as specified. Interaction evaluation asks whether an author can discover operations, express a procedure, understand errors, and recover without inspecting implementation code. Passing one kind of test does not answer the other. The report walkthrough exposes costs such as explicit bindings and deployment selection, but does not measure whether those costs are acceptable to new users. Similarly, structured diagnostics and inspection objects may help an agent avoid trial and error, but reduced retries, token use, and repair time remain hypotheses rather than measured outcomes. ## Falsifiability Criteria The implementation would fail its stated contracts if, for example: - a saved run could not identify the definition and bindings it used; - an invalid foreach boundary were accepted and executed as another region; - nested state writes were lost or counted twice; - resume reset the run-wide budget or resumed the wrong item; - condition evaluation used a different context model from validation; - ordinary source invocation required provider-specific graph routing. These cases support focused regression tests. Broader architectural claims, such as accommodating future source families without changing the runtime, remain design expectations to assess as those integrations are built. # Limitations The prototype demonstrates a workflow lifecycle under controlled conditions. Its main limitations concern how much authors must understand, which execution guarantees are provided, and how far the available evidence can be generalized. ## Authoring and Diagnosis Still Require Technical Knowledge The Python client reduces manual serialization and provides editable graphs and inspectable objects. It does not remove the need to understand schemas, state bindings, outcomes, and deployment selection. The report example makes this cost visible: a short procedure requires more declarations than direct function calls. Diagnostics identify many invalid structures and bindings, but an accurate message is not necessarily an understandable repair instruction. Authors still need to distinguish a graph error from an environment problem or a failed external operation. The current evidence does not establish that new users can make these distinctions without assistance. Inspection also requires judgment. A trace shows recorded execution, not whether a report is factually correct or a remote side effect was desirable. ## Execution Guarantees Have Defined Boundaries Foreach iteration, nested workflow scopes, structured context, and run-wide step budgets are implemented foundations. They do not yet provide general fork/gather control for arbitrary branches. In particular, concurrent iteration should not be presented as a solution to correlating branches that split, loop, and later meet at different gather points. Persisted interrupted runs can resume at explicit boundaries. This is not arbitrary mid-handler crash recovery, replay of every external call, or an exactly-once side-effect guarantee. A run's step limit bounds admitted graph steps; it does not bound a handler's execution time or the cost of its external requests. Schema validation checks declared structure, not business truth. A returned report may satisfy its schema while containing incorrect information. Similarly, a fixed graph specifies routing but does not make remote responses or concurrent completion order reproducible. ## Deployment and Trust Assumptions The controlled examples assume trusted operators and trusted Python sources. Python operations execute in the server process without a sandbox. The thesis does not establish multi-tenant isolation, role-based authorization, or production-grade credential management. An explicit workflow interrupt can request a response, but is not by itself an authenticated approval or access-control mechanism. Source bindings make environment choices inspectable rather than making workflows universally portable. A destination environment still needs compatible operations, credentials, and dependencies. Python sources are loaded at startup; changing their code requires a server restart. The shared provider interface does not yet unify every provider's administration, authentication, and live health behavior. Filesystem-backed stores support the demonstrated persistence paths. Their use does not establish production performance, cross-process contention behavior at scale, or disaster recovery. A future database implementation would still need to preserve the lifecycle's transaction and ownership contracts; changing the storage engine alone would not prove those properties. Scheduled execution uses the same deployment and run records as direct invocation. Its ownership model assumes one scheduler over the participating file stores and is supported only by the local/static server configuration. This bounds the deployment conditions under which the scheduling mechanism can be used; it is not a distributed execution service. The provider interface exposes callable operations. It does not reproduce a provider's interactive widgets or its complete user interface. ## Limits of the Evidence The deterministic report fixture demonstrates lifecycle integration, not broad document understanding or graph expressiveness. Targeted tests cover additional execution mechanisms. The evidence index identifies those tests; this report records execution results for the report-session suites. The in-process setup also limits the walkthrough to service composition; it does not test the displayed HTTP connection. Without independent authoring tasks or matched cross-system measurements, its results cannot establish ease of use, repair efficiency, or comparative performance. Those questions require the interaction evaluation described in Future Work. # Future Work The remaining questions concern richer execution semantics, the effectiveness of authoring and diagnosis, and operation beyond the controlled environment. ## Establish General Fork and Gather Semantics General fork/gather requires a rule for identifying which concurrent work belongs to the same invocation. Existing frames, scopes, iteration activations, and state lineages provide foundations, but their relationships must remain coherent through nested execution and resume. A fork creates concurrent execution branches; a gather must determine which arriving branches belong together before combining their state. Loops, partial gathers, and repeated visits make this more than waiting for a fixed number of arrivals. The design must also preserve contribution identity so that a write already included in one merge is not applied again in a later merge. This work remains planned, with reference-model verification preceding production implementation. Pressure cases should become executable tests for correlation, merge behavior, and recovery. The authoring contract also needs to make clear which graphs are rejected before execution and which decisions remain the author's responsibility. ## Evaluate the Authoring and Recovery Experience A focused usability study should ask participants to discover an operation, build a small workflow, change its contract, diagnose a broken binding, and inspect a failed or interrupted run. Useful measures include task completion, time to a correct repair, unnecessary retries, and reliance on source-code inspection. Execution and interaction should be evaluated together without conflating them. For example, a validation rule may correctly reject a graph while its diagnostic fails to explain the ownership boundary that was crossed. Conversely, a convenient editing operation must not hide a change to the workflow's execution meaning. The Python client and CLI should receive evidence appropriate to their own interaction styles. For agent trials, fix the interface, allowed operations, task fixtures, and success criteria before collecting results. Check an agent's report against saved artifacts, deployment identity, run output, and recorded interactions. Human evaluation can test whether the lifecycle vocabulary and data-binding model are understandable without implementation knowledge. ## Durable Waiting Within a Run Scheduling starts a new run of a saved deployment. Waiting until a time or event during an existing run would instead require a durable suspension point and rules for resuming it. Future work should distinguish these two forms of timed execution rather than treat a wait operation as another schedule. Distributed execution would additionally require an ownership model beyond the current single-scheduler arrangement. ## An Assistant-Backed Authoring Application The surrounding application is intended to combine assistant-backed chat with workflow administration. A shell can let an assistant retain Python objects across interactions, while typed client objects can support dedicated views of artifacts, deployments, and runs. The design question is how to let an author inspect and correct a proposed procedure before executing it, while keeping subsequent run status and requests for input understandable. This application requires an interaction design and evaluation of how the assistant uses the public client to construct, revise, and inspect workflows. ## Preserving Contracts Across Operational Changes Additional providers and storage backends would test whether the architectural boundaries hold beyond the demonstrated implementations. The question is whether an integration can preserve source compatibility, run identity, and recovery behavior without changing the graph's execution rules. Such work needs failure tests and deployment evidence as well as a working adapter. Richer debugging should clarify what can safely be resumed or repeated, especially around external side effects. Showing more trace information is different from promising that an earlier action can be undone. # Conclusion This report examined how a useful procedure can become a reusable workflow that an author can define and an operator can inspect. The implemented system separates the saved definition, its environment bindings, and each execution into artifacts, deployments, and runs. Those distinctions give workflow use a record beyond the lifetime of an editing session or a single script invocation. The graph model separates data movement from control movement. Contracts and bindings describe what a step receives and writes; outcomes choose transitions; explicit runtime constructs govern iteration, child scopes, and interruption. The Python client exposes this model through authoring and inspection objects, while the API and provider boundaries connect it to configured operations. The report case demonstrates that these representations compose into a save–deploy–run–inspect lifecycle for the fixed fixture. The repository also contains targeted tests covering execution requirements X1–X5; the evidence index locates them without reporting a combined execution result. For the authoring objectives R1–R5, the work identifies and exercises supporting interfaces, but their effectiveness for independent human or agent authors remains an open evaluation question. Explicit contracts expose data mappings, saved versions, and run records, while requiring authors to learn more concepts than a sequence of function calls. The implemented contribution is the integration of these contracts into a programmable lifecycle, demonstrated by saving, configuring, executing, and inspecting the report procedure through the public client. # References {#sec:refs .unnumbered} ::: {#refs} ::: \appendix # Evidence Index This appendix maps the evaluation's evidence identifiers to implementation and tests. Paths identify inspectable evidence; they are not a claim that all listed suites passed in one newly recorded full-system run. ## Core Workflow Lifecycle E1: artifacts, deployments, stopped runs, and explicit resume boundaries. - `src/wf_artifacts/models.py` - `src/wf_artifacts/runs/` - `src/wf_api/run_lifecycle.py` - `tests/wf_api/test_artifact_api.py` - `tests/wf_api/test_run_api.py` ## Python Authoring and Inspection E2: reconstructed client objects, editable workflows, and the report fixture. - `src/wf_client/` - `tests/wf_client/test_authoring.py` - `tests/wf_client/test_deployments.py` - `tests/wf_client/test_runs.py` - `examples/report_workflow/` - `tests/examples/test_report_workflow_example.py` - `tests/examples/test_thesis_python_walkthrough.py` The example's README retains the command-line route for operators who need it. That alternative interface is not an additional evaluated case in the current thesis. ## Validation and Diagnostics E3: structural validation, source compatibility, and repair information. - `src/wf_core/validation/` - `src/wf_artifacts/validation.py` - `tests/artifacts/test_validation.py` - `tests/core/test_structured_context_validation.py` - `tests/core/test_foreach_control_regions.py` ## Execution Ownership and Budgets E4: nesting, structured context, concurrent iteration, and persisted limits. - `src/wf_core/runtime/` - `tests/core/test_foreach_back_edges.py` - `tests/core/test_concurrent_foreach_interrupts.py` - `tests/core/test_structured_runtime_context.py` - `tests/core/test_run_step_budget.py` - `tests/core/test_run_step_budget_codec.py` - `tests/core/test_run_step_budget_async.py` ## Source Provider Boundary E5: source contracts and provider-specific execution behind server composition. - `src/wf_platform/sources.py` - `src/wf_server/config.py` - `src/wf_sources_python/` - `src/wf_sources_mcp/` - `tests/wf_sources_python/test_loader.py` - `tests/wf_sources_mcp/test_runtime.py` - `tests/wf_transport_rpc_http/test_mcp_backed_server_rpc.py` The existing providers demonstrate this separation for their implemented operations. They do not establish equal lifecycle features across providers or prove compatibility with every future source family.