--- name: wf-python description: Use when writing, reviewing, or debugging Python code that uses wf_client App, RemoteCapability, EditableWorkflow, WorkflowArtifact, Deployment, Run, or the Python workflow lifecycle. Prefer this skill for Python workflow authoring, typed contracts, artifact editing, deployment selection, interrupts, and traces; use wf-cli instead for shell-first work. --- # wf Python Client Use `wf_client` as the public Python object API over the workflow service. Keep JSON-RPC payloads, codecs, stores, and draft workspaces below this boundary. ## Start With The Right Object ```python from wf_client import App app = App.from_http_jsonrpc("http://localhost:8765/rpc") capability = await app.capability("app.default.thing") ``` Choose the object that matches the operation: - `RemoteCapability`: inspect or directly probe one capability. - `EditableWorkflow`: author or revise a graph locally. - `WorkflowArtifact`: inspect one immutable saved version. - `Deployment`: bind one artifact version to concrete sources and validate it. - `Run`: inspect, refresh, resume, or trace one durable execution. Do not collapse artifact saving, deployment configuration, and execution into one invented "publish" operation. ## Prefer Python Contract Types Declare workflow contracts with Pydantic models or supported Python types. The builder converts them to canonical schemas. ```python from pydantic import BaseModel class Input(BaseModel): topic: str class State(BaseModel): result: str | None = None class Output(BaseModel): result: str graph = app.new_workflow( "report", input_schema=Input, state_schema=State, output_schema=Output, ) ``` Use raw schema dictionaries only when the caller already owns a JSON Schema or needs a construct the Python type system cannot express. ### Replace A Contract During Authoring Use `set_contract()` instead of assigning normalized builder fields directly: ```python graph.set_contract( state_schema=ExpandedState, output_schema=FinalOutput, outcomes=("completed", "rejected"), ) ``` Supplied fields replace the whole corresponding contract. Omitted fields remain unchanged. Replacement is atomic: normalization completes before the builder is mutated. Existing bindings remain, so run local validation after replacement to find paths invalidated by the new contract. ## Know What The Builder Infers `graph.use(remote_capability)` registers the capability's node contract. When bindings are omitted, it can auto-bind matching capability fields against the workflow's already-declared input and state fields. It does not invent the workflow's public contract or topology. Keep these explicit: - Input, state, and output contract declarations. - Workflow outcomes when they differ from the default `("ok",)`. - Entry point. - Routes and terminal nodes. - Final workflow-output projection. - Deployment bindings and selection when the environment is ambiguous. Prefer canonical `input=[...]` and `output=[...]` binding lists when the dataflow matters. Auto-binding is useful for exact field-name matches, not a substitute for deciding semantics. ## Author, Validate, Save ```python from wf_authoring import input_from, input_value, output_to, state_path step = graph.use( capability, id="constant", input=[input_value("value", "hello")], output=[output_to("value", state_path("result"))], ) end = graph.end("ok", id="end_ok") graph.set_entry_point(step) graph.connect(step, "ok", end) graph.set_output([input_from(state_path("result"), "result")]) local = graph.validate_local() local.raise_for_errors() validation = await graph.validate() validation.raise_for_errors() artifact = await graph.save(version=1, title="Report") ``` `validate_local()` performs no remote I/O. `validate()` adds server plan and dependency validation. `save()` validates, saves, then inspects the exact saved version before returning its immutable snapshot. ## Edit An Existing Artifact ```python artifact = await app.workflow("report", version=3) graph = artifact.edit() # Equivalent: graph = await app.edit_workflow("report", version=3) ``` Use ordinary builder methods on `graph`. Do not reconstruct an existing graph from scratch: the seeded editor preserves schemas, bindings, routes, outcomes, subgraphs, and retained dependency contracts. Save edits as a new immutable version unless the caller explicitly requests otherwise. Never guess step IDs or state paths. Inspect `artifact.inspect()` or the editable graph before choosing mutation targets. ## Deploy And Run ```python deployment = await artifact.deploy( "report.production", bindings={"logical.documents": "production.documents"}, ) readiness = await deployment.validate() if not readiness.runnable: for diagnostic in readiness.diagnostics: print(diagnostic.code, diagnostic.message) raise RuntimeError("deployment is not runnable") run = await deployment.run({"topic": "workflow systems"}) ``` `artifact.run(input)` is convenient only when deployment selection is unambiguous. Handle `DeploymentRequired` rather than guessing an account or source binding. Run snapshots are immutable: ```python run = await run.refresh() if run.status == "interrupted" and run.interrupt is not None: run = await run.resume({"approved": True}) trace = await run.trace(start=0, limit=25) for frame in trace.frames: print(frame) ``` Trace reads must stay bounded (`limit` is 1 through 100). ## Handle Public Errors Catch `WorkflowClientError` or its public subclasses. Do not import transport exceptions or decode wire payloads yourself. ```python from wf_client import ( DeploymentRequired, ProtocolError, TransportError, WorkflowClientError, ) try: run = await artifact.run(payload) except DeploymentRequired as error: print(error.candidate_deployment_ids, error.unresolved_logical_sources) except TransportError as error: print("workflow service unavailable", error) except ProtocolError as error: print(error.code, error.message, error.data) except WorkflowClientError as error: print(type(error).__name__, error) ``` ## Boundaries - `wf_client` intentionally has no draft workspace API. Use server/admin or console surfaces only when the task is genuinely about drafts. - Rich representations are inert debugging aids, not a secrecy boundary. Schema-level sensitivity metadata is the appropriate future source of truth; do not rely on repr redaction to protect credentials. - Use the public objects before inspecting `wf_api`, RPC clients, codecs, or stores. Drop below the client boundary only when implementing the client. Read [references/python-lifecycle.md](references/python-lifecycle.md) when a complete typed lifecycle or an editing/debugging recipe is needed.