wfpy — Pythonic agentic workflows on actor-dataflow semantics
A Pythonic framework for agentic workflows, built on actor-dataflow semantics: agents and tasks are actors in a dataflow graph, connected by FIFO queues and fired when their inputs are ready. Independent work runs concurrently by default, feedback loops are ordinary edges, and the graph is exportable as data.
New to dataflow? Read Dataflow concepts for the
model — actors, ports, tokens, firings, guards, schedules, priorities — and work
through the tutorial in examples/, which builds it up one runnable
step at a time.
wfpy is fully typed and ships py.typed for IDE support.
Install
pip install wfpy # the core: workflows, tasks, the CLI
pip install "wfpy[agent]" # + the @agent transports: HTTP API, MCP tools, ACP
Each release is published to PyPI and also attaches the built wheel. Use these to install a version that is not on PyPI, or without PyPI access:
# a release wheel straight from GitHub
pip install "wfpy @ https://github.com/huawei-csl/wfpy/releases/download/v1.1.0/wfpy-1.1.0-py3-none-any.whl"
# or a tagged source
pip install "git+https://github.com/huawei-csl/wfpy@v1.1.0"
For local development, clone and install editable: pip install -e ".[dev]".
wfpy run my_workflow.py # run a workflow
Agent connectors (ACP)
An @agent on the acp transport, and the chat in the wfpy IDE, talk to a
coding agent over the Agent Client Protocol.
Each such agent is a connector: a command wfpy spawns. wfpy knows four, and
a connector is available when its command is on the PATH:
| Connector | Command wfpy spawns | Install | Sign in |
|---|---|---|---|
opencode (default) |
opencode acp |
npm install -g opencode-ai (or curl -fsSL https://opencode.ai/install | bash) |
opencode auth login |
claude |
claude-agent-acp |
npm install -g @anthropic-ai/claude-code @agentclientprotocol/claude-agent-acp |
run claude once and /login, or set ANTHROPIC_API_KEY |
codex |
codex-acp |
npm install -g @zed-industries/codex-acp |
codex login, or set OPENAI_API_KEY |
gemini |
gemini --experimental-acp |
npm install -g @google/gemini-cli |
run gemini once and sign in, or set GEMINI_API_KEY |
claude-agent-acp is the ACP adapter for Claude Code: it drives Claude Code,
so it uses Claude Code's login and settings. Install the connectors you want.
Only opencode is needed for the defaults. The npm packages need Node.js 18 or
newer.
Set up a machine
# 1. wfpy with the agent transports (HTTP API, MCP tools, ACP)
pip install -e ".[agent]" # or "wfpy[agent] @ <wheel-url>"
# 2. the connectors (pick the ones you use)
npm install -g opencode-ai
npm install -g @anthropic-ai/claude-code @agentclientprotocol/claude-agent-acp
# 3. sign in once per agent
opencode auth login
claude # then /login inside it
# 4. check what wfpy sees
wfpy connectors
wfpy connectors lists every connector, whether it is available and where it
came from:
name available source model mode command
claude yes discovered - - claude-agent-acp
codex no discovered - - codex-acp
gemini no discovered - - gemini --experimental-acp
opencode yes discovered - - opencode acp
user file: ~/.config/wfpy/connectors.toml workspace file: <project>/.wfpy/connectors.toml
A connector reported no while it is installed is not on the PATH of the
process that ran wfpy. This happens with nvm, which puts global npm binaries
under ~/.nvm/versions/node/<version>/bin. When VS Code is started from a
desktop launcher instead of a shell, it may not see that directory. Either
start VS Code from a shell, or give the full path in a connectors file (below).
Choose a connector
- One agent:
@agent(transport="acp", connector="claude"). - A run's default:
wfpy run flow.py --acp-connector claude. Agents that name no connector use it. Without the flag the default isopencode. - An explicit command line:
--agent-cli-acp-command "claude-agent-acp"wins over both. - The wfpy IDE: the
wfpy.acp.connectorsetting chooses the connector for the chat and for runs started from the editor (defaultopencode).
Open an agent's session in its own CLI
The claude and opencode adapters use the agent's own session id as the ACP
session id. So a session can be continued in the agent's CLI, for example
claude --resume <id> (started from the session's directory) or
opencode -s <id>. Each firing records the session it used, as
{"id", "connector", "cwd"}:
- in the
sessionfield of itsagent.message.endrun event; - as
agentSessionon the agent's reply in its chat history.
The session's directory is where wfpy ran. In the wfpy IDE, the chat header
and each finished turn of a run's agent have a button that opens the session in
a terminal, and another that copies its id. A connector can give its own
command as resume = "... {sessionId}" in a connectors file. resume = ""
removes it, and then the id is only copied.
Configure connectors
Two optional TOML files add connectors or override the discovered ones. Both
are read by wfpy and by the wfpy IDE:
~/.config/wfpy/connectors.toml(or$XDG_CONFIG_HOME/wfpy/connectors.toml) holds this machine's settings, such as paths and your model choices..wfpy/connectors.toml, at the project root (the directory withpyproject.tomlor.git), is checked in with the project and overrides the user file.
[connectors.claude]
command = "claude-agent-acp" # argv, split as a shell would
model = "sonnet" # a model the agent offers
mode = "acceptEdits" # a session mode the agent offers
[connectors.opencode]
command = "/opt/opencode/bin/opencode acp"
http_api = true # OpenCode's HTTP API beside ACP
[connectors.claude-work] # a second Claude connector, with its own env
command = "claude-agent-acp"
env = { ANTHROPIC_API_KEY = "..." }
Replicate on another machine
- Install wfpy with
[agent]and the same connectors (step 1 and step 2 above). Check versions on the working machine withnpm ls -g --depth=0and pin them withnpm install -g <pkg>@<version>. - Copy
~/.config/wfpy/connectors.toml, if you have one. Fix the paths in it for the new machine. Do not copy API keys into a file that is checked in. - Sign in to each agent on the new machine (step 3). Logins are not copied with the files.
- Run
wfpy connectorsfrom inside the project. Every connector you use should showavailable yes.
The project's .wfpy/connectors.toml comes with the repository, so nothing
else is needed for it.
Minimal example
from wfpy import task, workflow, connect, Port
@task
class Doubler:
factor: int # parameter
class Ports:
In = Port[int]()
Out = Port[int]()
def action(self, x: int) -> int:
return x * self.factor
@workflow
def double_pipeline():
d1 = Doubler(factor=2)
d2 = Doubler(factor=3)
connect("Input", d1.In)
connect(d1.Out, d2.In)
connect(d2.Out, "Output")
Features
| Feature | Status |
|---|---|
@task with typed ports |
✅ |
Multi-action tasks with @action / @guard |
✅ |
@workflow with connect() wiring |
✅ |
| FIFO round-robin scheduler | ✅ |
@tool(cmd=...) external process tasks |
✅ |
@agent(prompt=...) LLM tasks |
✅ |
@agent(transport="mock") offline agents (no model, no credentials) |
✅ |
@context(read=[...], write=[...]) task context policy |
✅ |
@viewer(action_name=..., inputs=[...], viewType=...) sink/view tasks |
✅ |
@source(viewType=...) one resource — file, folder or URL — emitted once |
✅ |
| Nested workflows | ✅ |
CLI (wfpy run) |
✅ |
| JSON plan IR export | ✅ |
Running agents offline
transport="mock" fires an @agent actor without a model, a network call or
any credentials. The actor still obeys the normal firing rules and still emits a
value on every declared output port, so the shape of a graph — fan-out and
join, repair feedback loops, guard routing, firing order — can be tested in CI
without spending tokens or depending on how a model happens to word things.
@agent(prompt="Summarize the input.", transport="mock",
mock_outputs={"Summary": "a concise summary"})
class Summarizer:
class Ports:
In = Port[str](direction="in")
Summary = Port[str](direction="out")
Score = Port[int](direction="out") # unlisted → synthesized as 0
Ports absent from mock_outputs are filled in from their declared type, so an
int port receives an int and a File port is materialized as a real file.
Swapping back to a real transport changes nothing else about the graph.
Shared context decorator
Use @context(...) to attach shared-context read/write policy metadata to task
classes. It can be applied directly to a class or stacked with @task in
either order.
@guard(...) predicates are supported on both explicit @action(...) methods
and implicit default action(...) methods.
Architecture
wfpy/
├── types.py # Port, Resource, File, Map type descriptors
├── core.py # @task, @action, @guard, @workflow, @agent, @tool decorators
├── graph.py # connect(), WorkflowGraph builder
├── runner.py # FIFO round-robin scheduler
├── cli.py # CLI entry-point (wfpy run ...)
└── __init__.py # public API re-exports
Documentation lives under docs/: the dataflow model and the sidecar.
Resource draft
wfpy includes a draft Resource value type for non-file locators (for example
URLs, folders, and endpoint-like references):
from wfpy import Port, Resource
class Ports:
In = Port[Resource](direction="in")
Out = Port[Resource](direction="out", ext=".json")
File remains supported and is treated as a file-focused specialization of
Resource for backward compatibility.
Kind helpers
wfpy also exposes basic helpers for classifying resource locators:
from wfpy import (
infer_resource_kind,
is_url_resource,
is_http_resource,
is_folder_resource,
is_file_resource,
)
infer_resource_kind("https://example.com/data") # "http"
is_url_resource("s3://bucket/key") # True
is_folder_resource("/tmp/my-dir/") # True
Agent skills (SKILL.md) draft
wfpy supports name-based skill lookup for agents:
from wfpy import agent, Port
@agent(
skill="writer",
use_skill=True,
use_prompt=True,
use_skill_hooks=True,
prompt="Focus on concise release notes."
)
class Writer:
class Ports:
In = Port[dict](direction="in")
Out = Port[dict](direction="out")
Skill names resolve by convention under your runtime root:
.wf/skills/<skill-name>/SKILL.md.wf/skills/<skill-name>/scripts/<hook>.py|.sh
wfpy also searches Claude-compatible skill roots in order:
<runtime-root>/.claude/skills/<skill-name>/SKILL.md<cwd>/.wf/skills/<skill-name>/SKILL.md<cwd>/.claude/skills/<skill-name>/SKILL.md~/.claude/skills/<skill-name>/SKILL.md
Prompt composition order:
SKILL.mdbody (ifuse_skill=True)prompt(ifuse_prompt=True)- wfpy runtime instruction (always appended by runtime)
Missing enabled skill files are runtime errors.
Claude agent format compatibility (draft)
wfpy agents can also reference Claude-style markdown agent profiles by name:
from wfpy import agent, Port
@agent(
claudeAgent="code-reviewer",
useClaudeAgent=True,
usePrompt=True,
prompt="Adapt output to this workflow's output ports."
)
class Reviewer:
class Ports:
In = Port[dict](direction="in")
Out = Port[dict](direction="out")
Discovery roots (ordered):
<runtime-root>/.claude/agents<cwd>/.claude/agents~/.claude/agents
Supported profile fields in compatibility mode:
name,description, markdown body prompt,skills
Ignored with non-fatal warnings:
model(wfpy keeps the current model/provider selection)tools,disallowedTools,permissionMode,maxTurns,mcpServers,hooks,memory,background,effort,isolation
Metadata
Release files for wfpy 1.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| wfpy-1.1.0.tar.gz | 339.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| wfpy-1.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 595.4 kB
Release files / wfpy-1.1.0.tar.gz
| Download URL | wfpy-1.1.0.tar.gz |
|---|---|
| Size | 339.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
fecd2d15df36e188b51fc5b99362d6defe517ca9230426c37c3d8c53d349b76c
|
|
BLAKE2b-256 checksum How to use checksums |
578627f8b3f7193bb387e00858f1014a9d6925772132426705550b43fdd3ad43
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency logRelease files / wfpy-1.1.0-py3-none-any.whl
| Download URL | wfpy-1.1.0-py3-none-any.whl |
|---|---|
| Size | 255.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3a96f2ca167918237c749a198310ac15fa3ef6c5e5b1c9b506e33c931b3cd0df
|
|
BLAKE2b-256 checksum How to use checksums |
a0a24317c62cb6a406aac7261d7579d66762d0f7669989f7d78bbec8352ff714
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency log