1804 lines
82 KiB
Markdown
1804 lines
82 KiB
Markdown
---
|
||
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:
|
||
|
||
<!-- markdownlint-disable-next-line MD013 -->
|
||
```{.mermaid #fig:report-branch width=95% caption="Alternative routes to report rendering."}
|
||
%%{init: {"flowchart": {"rankSpacing": 20, "nodeSpacing": 20}}}%%
|
||
flowchart LR
|
||
Read["Read<br/>notes"] --> Extract["Extract<br/>report"]
|
||
Extract --> Check{Complete?}
|
||
Check -->|ready| Render["Render<br/>report"]
|
||
Check -->|needs_information| Ask["Request<br/>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.
|
||
|
||
<!-- markdownlint-disable-next-line MD013 -->
|
||
```{.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.
|
||
|
||
<!-- markdownlint-disable-next-line MD013 -->
|
||
```{.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.
|
||
|
||
<!-- markdownlint-disable-next-line MD013 -->
|
||
```{.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.
|
||
|
||
<!-- markdownlint-disable-next-line MD013 -->
|
||
```{.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.
|
||
|
||
<!-- markdownlint-disable-next-line MD013 -->
|
||
```{.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,<br/>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<br/>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.
|
||
|
||
<!-- markdownlint-disable-next-line MD013 -->
|
||
```{.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 -->
|
||
# References {#sec:refs .unnumbered}
|
||
|
||
::: {#refs}
|
||
:::
|
||
|
||
\appendix
|
||
<!-- Appendices -->
|
||
# 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.
|