201 lines
5.7 KiB
Markdown
201 lines
5.7 KiB
Markdown
# Python Workflow Lifecycle
|
|
|
|
This reference contains complete patterns for the public `wf_client` API.
|
|
|
|
## Typed Authoring With Contract Replacement
|
|
|
|
```python
|
|
from pydantic import BaseModel
|
|
|
|
from wf_authoring import input_from, input_value, output_to, state_path
|
|
from wf_client import App
|
|
|
|
|
|
class InitialInput(BaseModel):
|
|
request_id: str
|
|
|
|
|
|
class InitialState(BaseModel):
|
|
value: str | None = None
|
|
|
|
|
|
class InitialOutput(BaseModel):
|
|
value: str
|
|
|
|
|
|
class ExpandedState(BaseModel):
|
|
value: str | None = None
|
|
source: str | None = None
|
|
|
|
|
|
class FinalOutput(BaseModel):
|
|
value: str
|
|
|
|
|
|
app = App.from_http_jsonrpc("http://localhost:8765/rpc")
|
|
constant = await app.capability("wf.std.constant")
|
|
|
|
graph = app.new_workflow(
|
|
"typed_example",
|
|
input_schema=InitialInput,
|
|
state_schema=InitialState,
|
|
output_schema=InitialOutput,
|
|
)
|
|
step = graph.use(
|
|
constant,
|
|
id="constant",
|
|
input=[input_value("value", "hello")],
|
|
output=[output_to("value", state_path("value"))],
|
|
)
|
|
|
|
graph.set_contract(state_schema=ExpandedState, output_schema=FinalOutput)
|
|
|
|
end = graph.end("ok", id="end_ok")
|
|
graph.set_entry_point(step)
|
|
graph.connect(step, "ok", end)
|
|
graph.set_output([input_from(state_path("value"), "value")])
|
|
|
|
graph.validate_local().raise_for_errors()
|
|
validation = await graph.validate()
|
|
validation.raise_for_errors()
|
|
artifact = await graph.save(version=1, title="Typed example")
|
|
run = await artifact.run({"request_id": "request-1"})
|
|
```
|
|
|
|
## Saved Workflow As A Native Subgraph
|
|
|
|
Load the exact child artifact before authoring the parent. Its public contract
|
|
is copied into the parent boundary for local validation; its artifact ID and
|
|
version remain the runtime dependency.
|
|
|
|
```python
|
|
from wf_authoring import input_from, input_path, output_to, state_path
|
|
|
|
child = await app.workflow("child", version=2)
|
|
parent = app.new_workflow(
|
|
"parent",
|
|
input_schema=ParentInput,
|
|
state_schema=ParentState,
|
|
output_schema=ParentOutput,
|
|
)
|
|
run_child = parent.subgraph(
|
|
child,
|
|
id="run_child",
|
|
input=[input_from(input_path("prompt"), "prompt")],
|
|
output=[output_to("value", state_path("result"))],
|
|
)
|
|
parent.set_entry_point(run_child)
|
|
parent.connect(run_child, "ok", parent.end("ok", id="parent_done"))
|
|
parent.set_output([input_from(state_path("result"), "result")])
|
|
|
|
parent.validate_local().raise_for_errors()
|
|
(await parent.validate()).raise_for_errors()
|
|
parent_v1 = await parent.save(version=1)
|
|
|
|
deployment = await parent_v1.deploy("parent.production")
|
|
readiness = await deployment.validate()
|
|
if not readiness.runnable:
|
|
raise RuntimeError(readiness.diagnostics)
|
|
run = await deployment.run({"prompt": "hello"})
|
|
```
|
|
|
|
Choose binding paths from `child.inspect()` and the parent models. Saving the
|
|
parent does not duplicate the child plan: the saved parent retains the exact
|
|
`child.v2` dependency, which deployment validation and execution resolve.
|
|
|
|
## Lossless Editing
|
|
|
|
```python
|
|
artifact = await app.workflow("report", version=3)
|
|
workflow = artifact.inspect()
|
|
print([node.id for node in workflow.nodes])
|
|
|
|
summarize = await app.capability("app.default.summarize")
|
|
graph = artifact.edit()
|
|
|
|
summary = graph.use(
|
|
summarize,
|
|
id="summarize",
|
|
input=[input_from(state_path("draft"), "text")],
|
|
output=[output_to("summary", state_path("summary"))],
|
|
)
|
|
graph.set_route("draft_report", "ok", summary)
|
|
graph.connect(summary, "ok", "end_ok")
|
|
|
|
graph.validate_local().raise_for_errors()
|
|
(await graph.validate()).raise_for_errors()
|
|
report_v4 = await graph.save(version=4)
|
|
```
|
|
|
|
The step IDs and paths above are examples. Inspect the loaded artifact and use
|
|
its real contract; do not assume those names exist.
|
|
|
|
## Deployment Diagnosis And Durable Runs
|
|
|
|
```python
|
|
from wf_client import DeploymentRequired, ProtocolError, WorkflowClientError
|
|
|
|
artifact = await app.workflow("invoice", version=2)
|
|
|
|
try:
|
|
run = await artifact.run({"invoice_id": "INV-1001"})
|
|
except DeploymentRequired as error:
|
|
print("candidates", error.candidate_deployment_ids)
|
|
print("unresolved", error.unresolved_logical_sources)
|
|
for diagnostic in error.diagnostics:
|
|
print(diagnostic.code, diagnostic.message)
|
|
except ProtocolError as error:
|
|
print("server error", error.code, error.message, error.data)
|
|
raise
|
|
except WorkflowClientError as error:
|
|
print(type(error).__name__, error)
|
|
raise
|
|
|
|
deployment = await artifact.deploy(
|
|
"invoice.production",
|
|
bindings={"billing": "production.billing"},
|
|
)
|
|
readiness = await deployment.validate()
|
|
if not readiness.runnable:
|
|
for diagnostic in readiness.diagnostics:
|
|
print(diagnostic.code, diagnostic.message)
|
|
raise RuntimeError("invoice.production is not runnable")
|
|
|
|
run = await deployment.run({"invoice_id": "INV-1001"})
|
|
|
|
if run.status == "interrupted" and run.interrupt is not None:
|
|
run = await run.resume({"approved": True})
|
|
|
|
run = await run.refresh()
|
|
trace = await run.trace(start=0, limit=25)
|
|
for frame in trace.frames:
|
|
print(frame)
|
|
```
|
|
|
|
Catch the specific public errors useful to the application and retain a final
|
|
`WorkflowClientError` fallback. Unknown server errors remain inspectable
|
|
`ProtocolError` values with `code`, `message`, and `data`.
|
|
|
|
## Structured foreach context
|
|
|
|
Prefer declared input bindings via the foreach reference:
|
|
|
|
```python
|
|
orders = graph.foreach(
|
|
id="orders",
|
|
over=state_path("orders"),
|
|
as_="order",
|
|
)
|
|
charge = graph.use(
|
|
charge_order,
|
|
input=[input_from(orders.item, "order")],
|
|
)
|
|
graph.set_route(orders, "loop", charge)
|
|
graph.set_route(charge, "ok", orders)
|
|
```
|
|
|
|
Normal capabilities receive foreach values through declared inputs. Advanced
|
|
handlers may inspect `ctx.foreach["orders"].index` and stable runtime
|
|
identities. Child workflows do not inherit caller context and must receive
|
|
input. `loop_item`, `loop_index`, and aliases are migration conveniences.
|