Use Weave from Python, one step at a time¶
This tutorial is for Python developers who want their application to create and
run workflows. You write the same small workflow in YAML and in Python, check
that both compile to the same result without a server, and then use the Python
client to publish, activate, and run it on a platform. The workflow receives
{"message": "Hello from Python"} and returns that object.
Before you start, you need:
- Python 3.12 or later, and either a Weave source checkout with
uv, or an
application environment where Weave is installed with its
clientextra. - For steps 1 to 5, nothing else: they run on your computer.
- For step 6, a running platform and an identity allowed to publish definitions
and start runs: your team's API with an access token, or a
local platform where you ran
weave platform demo.
You do not need a worker: a transform step runs inside Weave.
1. Choose the Python tool for the job¶
| You want to… | Use | Where it runs |
|---|---|---|
| Describe a workflow in a text file | YAML | Your editor; the compiler checks it |
| Generate a definition from Python | WorkflowBuilder and contract models |
Your Python process, offline |
| Publish definitions or start/read runs | WeaveClient |
Your application calls the API |
| Execute custom business logic | Worker with an async handler |
A separately admitted worker process |
| Add a reusable integration adapter | A connector package | The operator's native executor |
WorkflowBuilder constructs data. Its methods do not call your business functions
while a workflow runs. You will see the actual Python worker boundary at the end.
The SDK reference lists the complete API.
2. Prepare an application environment¶
Use Python 3.12 or later. Install Weave in the environment
that will run these scripts; an isolated CLI installation does not make
firefly_weave importable by another Python interpreter. For remote API calls,
install the matching release wheel with its client extra.
For this source-checkout route, start in the repository root. Select a separate environment for this lab so another checkout environment is not changed:
# Keep this lab's interpreter and dependencies together.
export UV_PROJECT_ENVIRONMENT="$PWD/.local/sdk-tutorial-venv"
uv sync --locked --python 3.12 --no-editable --extra client
mkdir -p .local/sdk-tutorial
# These shell functions reuse that interpreter for every command below.
python_sdk() { uv run --locked --no-editable --extra client python "$@"; }
weave_sdk() { uv run --locked --no-editable --extra client weave "$@"; }
python_sdk --version
Expected: Python 3.12 or later. Keep this terminal and repository directory for
the whole tutorial. If using an already installed application environment, define
python_sdk() { python "$@"; } and weave_sdk() { weave "$@"; } there instead,
then create .local/sdk-tutorial. The two functions must select the same install.
3. Write the YAML version¶
Save this as .local/sdk-tutorial/message.workflow.yaml:
apiVersion: weave/v1alpha1 # Select the definition language, not a package version.
kind: Workflow
metadata:
name: sdk-message
version: 1.0.0 # Published versions are immutable; change this when behavior changes.
spec:
inputSchema:
type: object
properties:
message: {type: string}
required: [message]
additionalProperties: false
outputSchema:
type: object
properties:
message: {type: string}
required: [message]
additionalProperties: false
steps:
- id: echo
kind: transform # Copy data inside Weave; this needs no external worker.
value: {ref: /input}
output: {ref: /steps/echo/output}
The schemas describe the accepted input and final output. required requires the
message; additionalProperties: false catches accidental extra fields. echo
is a local step ID. /input reads the whole input object, and
/steps/echo/output reads that step's result. These are data references.
4. Build the equivalent definition in Python¶
Save this complete script as .local/sdk-tutorial/build_message.py:
import json
from pathlib import Path
from firefly_weave.compiler.api import compile_source
from firefly_weave.compiler.catalog import CatalogSnapshot
from firefly_weave.contracts.definitions import RefExpression, TransformStep
from firefly_weave.sdk.builder import WorkflowBuilder
root = Path(__file__).parent
message_schema = {
"type": "object",
"properties": {"message": {"type": "string"}},
"required": ["message"],
"additionalProperties": False,
}
# The builder returns a new value after each change, so retain that return value.
builder = WorkflowBuilder(
"sdk-message", "1.0.0",
input_schema=message_schema,
output_schema=message_schema,
output=RefExpression(ref="/steps/echo/output"),
).add_step(
TransformStep(id="echo", kind="transform", value=RefExpression(ref="/input"))
)
# Both formats use the same compiler and the same explicit dependency catalog.
catalog = CatalogSnapshot.empty() # This workflow references no Actions or Connectors.
from_yaml = compile_source(
(root / "message.workflow.yaml").read_text(), format="yaml", catalog=catalog,
)
from_python = compile_source(builder.to_document(), format="object", catalog=catalog)
for result in (from_yaml, from_python):
if not result.ok or result.artifact is None:
for diagnostic in result.diagnostics:
print(diagnostic.code, diagnostic.path)
raise SystemExit("Correct the definition before continuing")
assert from_yaml.artifact.digest == from_python.artifact.digest
# JSON is portable definition data; no Python callback is serialized.
(root / "message.workflow.json").write_text(json.dumps(builder.to_document(), indent=2) + "\n")
(root / "message.artifact.json").write_bytes(from_python.artifact.to_bytes())
print("YAML and Python compile to the same executable digest")
print(from_python.artifact.digest)
Run it:
# No server, token, or database is contacted by this script.
python_sdk .local/sdk-tutorial/build_message.py
Expected: YAML and Python compile to the same executable digest, followed by a
64-character digest. You also get message.workflow.json (definition data) and
message.artifact.json (compiled data). The builder supplies canonical field
names and validates model structure. Compilation still checks references,
dependencies, and semantic rules.
Try changing the builder's output reference to /steps/missing/output and run
again. A compiler diagnostic identifies the missing reference. Restore echo
before continuing. Successfully constructing a builder is not proof that the
workflow compiles.
5. Execute it in the local simulator¶
Save .local/sdk-tutorial/make_simulation.py:
import json
from pathlib import Path
root = Path(__file__).parent
request = {
"artifact": json.loads((root / "message.artifact.json").read_text()),
"input": {"message": "Hello from Python"},
"mocks": {}, # No task responses are needed for an internal transform.
"now": "2026-01-01T00:00:00Z", # A fixed clock makes this simulation repeatable.
}
(root / "simulation.json").write_text(json.dumps(request, indent=2) + "\n")
# Write the simulation request next to the compiled artifact.
python_sdk .local/sdk-tutorial/make_simulation.py
# The simulator applies one input to the compiled artifact.
weave_sdk workflow simulate .local/sdk-tutorial/simulation.json --output json
Expected: status: "succeeded", with
variables.output equal to {"message": "Hello from Python"}. This is an
in-memory simulation. The next step creates a durable API run.
6. Connect your Python application to an API¶
Your script needs three things: the API address, a scope (the tenant, project, and environment it works in), and an access token. Choose the source that matches your platform:
- A team-operated API: your operator supplies the address, the three scope IDs, and a way to obtain a current access token, as in the connect guide's explicit mode. Set the variables in the table below.
- A local platform: complete the local platform guide
through
weave platform demo, then use the local-platform adaptation after the script. - A platform you saved with
weave auth setup: since 0.1.0a7, the SDK can reuse its server, sign-in, and workspace instead of these variables. Build the client from it as shown in Reuse your saved platform.
A team-operated API uses these variables:
| Variable | Meaning |
|---|---|
WEAVE_BASE_URL |
Exact API origin, for example https://weave.example.com; no /api/v1 suffix |
WEAVE_TENANT_ID |
Your tenant UUID |
WEAVE_PROJECT_ID |
The project UUID inside that tenant |
WEAVE_ENVIRONMENT_ID |
The environment UUID inside that project |
WEAVE_ACCESS_TOKEN |
A current access token issued for this API |
Remote origins use HTTPS; loopback HTTP is accepted for local development. Your
identity needs catalog.read, compile, definition.publish,
release.activate, run.start, and run.read. It cannot grant these to itself.
The SDK reads a saved platform only when your code asks for it, as in the
saved-platform option above. To let a person sign in from your own application,
with a browser or a code, use the OAuth session integration.
Save .local/sdk-tutorial/run_message.py. The complete script below uses the
supplied-token option. Local-platform users replace the scope/token block using
the small adaptation immediately after it.
import asyncio
import os
from pathlib import Path
from firefly_weave.contracts.access import Scope
from firefly_weave.contracts.catalog import ActivationRequest
from firefly_weave.contracts.runtime import StartRunRequest
from firefly_weave.sdk.client import WeaveClient
scope = Scope.model_validate({
"tenant_id": os.environ["WEAVE_TENANT_ID"],
"project_id": os.environ["WEAVE_PROJECT_ID"],
"environment_id": os.environ["WEAVE_ENVIRONMENT_ID"],
})
def access_token():
# Read on each request so a host can replace credentials without rebuilding the client.
# This callback does not acquire or refresh an expired token.
return os.environ["WEAVE_ACCESS_TOKEN"]
async def main():
source = (Path(__file__).parent / "message.workflow.yaml").read_text()
async with WeaveClient(os.environ["WEAVE_BASE_URL"], access_token, scope) as client:
# Check against this project's catalog before attempting publication.
catalog = await client.catalog()
checked = await client.compile(source=source, format="yaml", catalog=catalog, strict=True)
if not checked.ok:
for diagnostic in checked.diagnostics:
print(diagnostic.code, diagnostic.path)
raise SystemExit("The server rejected the definition")
version = await client.publish(
"workflows", source, "yaml", idempotency_key="sdk-message-publish-1",
)
activation = await client.activate(
ActivationRequest(version_id=version.id, artifact_digest=version.digest, scope=scope),
idempotency_key="sdk-message-activate-1",
)
run = await client.start_run(
StartRunRequest(activation_id=activation.id, input={"message": "Hello from Python"}),
idempotency_key="sdk-message-run-1",
)
print("activation_id:", activation.id)
print("run_id:", run.id)
# Start returns an admitted run; read its state to observe execution.
for _ in range(20):
current = await client.read_run(run.id)
if current.state.status == "succeeded":
print("output:", current.state.output)
return
if current.state.status in {"failed", "cancelled", "timed_out", "suspended"}:
raise SystemExit("Inspect run history; state is " + current.state.status)
await asyncio.sleep(0.5)
raise SystemExit("Still running; inspect the printed run_id before starting another run")
if __name__ == "__main__":
asyncio.run(main())
Use the local platform instead¶
Run these commands in the tutorial terminal from step 2, not in the terminal that runs the API. They restore the installation's non-secret session paths and renew its private host token, which the script reads when you run it. The commands assume the platform was set up from this same checkout:
# The API keeps running in its own terminal; restore this installation's paths and ports.
source .local/platform/session.env
# Renew the verified host token in its private file.
weave platform token
# The script reads the API address from WEAVE_BASE_URL.
export WEAVE_BASE_URL="$WEAVE_API_URL"
Expected: Token file: with the path of host-token.json. If the installation
is in another checkout or a custom directory, source that directory's
session.env by its absolute path and pass the same directory to
weave platform --directory /absolute/private/path token. With a
manual installation, restore its session and renew its token as
in its resume procedure, then
run export WEAVE_BASE_URL="$WEAVE_API_URL"; its first-run.json and
host-token.json have the same shape.
In run_message.py, replace the scope = ... assignment and access_token
function with this block. Keep the rest of main unchanged:
import json
# Reuse real scope IDs from platform demo; do not guess or copy example UUIDs.
platform = Path(os.environ["WEAVE_WORK_DIR"])
scope = Scope.model_validate(json.loads((platform / "first-run.json").read_text())["scope"])
def access_token():
# Reread the private file so a later platform token command can rotate it.
return json.loads((platform / "host-token.json").read_text())["access_token"]
The local option needs no WEAVE_ACCESS_TOKEN environment variable. The
callback reads credentials but does not refresh them itself.
Run the script¶
Whichever source you chose, run the script once:
# This step publishes, activates, and starts one durable run in your chosen scope.
python_sdk .local/sdk-tutorial/run_message.py
Expected: two UUIDs and output: {'message': 'Hello from Python'}. Keep the
activation ID if you want to try the inbound webhook in the
Build a custom integration tutorial.
Read the three mutations as separate decisions:
| Call | What you get | Why the next step needs it |
|---|---|---|
publish |
Immutable definition version ID and digest | Activation identifies the exact compiled definition |
activate |
Environment activation ID | Starting a run selects a prepared environment binding |
start_run |
Run ID | Reads and history follow this particular execution |
The fixed idempotency keys let you retry the same requests without intentionally creating a second execution. Use a new run key for a new input or intentional new run. Changed source needs a new definition version and publication/activation keys. If a request times out, its mutation may already have committed; retain its key and inspect resource state before retrying.
7. Submit the Python-built version instead¶
publish accepts YAML or JSON text. To publish the definition generated in step
4, replace the source assignment in run_message.py with:
# Serialize definition data; the API does not execute this application's Python.
source = (Path(__file__).parent / "message.workflow.json").read_text()
Change format="yaml" to format="json" in compile and the "yaml"
argument to "json" in publish. Use new publication and activation idempotency
keys because the request representation changed. The definitions have the same
executable digest. To create a separate named example, change the builder's name
and rebuild before publishing. Continue to select the returned IDs, not guessed
UUIDs or locally calculated version IDs.
8. Know where custom Python actually executes¶
When a step must run business code, publish an Action with a worker task contract
and admit a release implementing it. The complete worker tutorial
uses example-record@1.0.0. Its handler returns an object containing receipt
and customer. This small handler illustrates the return shape without making
an external request:
async def record(lease):
# lease.input has already passed the admitted task's input schema.
return {"receipt": "accepted", "customer": lease.input["customer"]}
After authentication and worker registration, that handler is registered by exact capability name:
from firefly_weave.sdk.worker import Worker
# transport must be the authenticated WorkerTransport for an admitted instance.
worker = Worker(transport, {"example-record@1.0.0": record}, concurrency=1)
await worker.run()
This last block is a wiring excerpt, not a standalone program. Use the complete
worker example for credentials, registration,
shutdown, and external effects. That example passes lease.operation_key to the
receiver for deduplication. A workflow Action names the task contract; it never
contains a Python import path. The SDK manages claims, heartbeat renewal, and
completion around the handler.
Troubleshoot one boundary at a time¶
| Symptom | What to check |
|---|---|
ModuleNotFoundError |
Run the script with the interpreter containing Weave and the client extra |
| A missing environment variable | Restore all five variables from step 6 in this terminal |
| Compilation reports a missing Action | Fetch the correct project catalog; publish/admit dependencies before compiling |
| HTTP 401 | Token expiration, issuer, and audience; this example does not refresh tokens |
| HTTP 403 | The identity's current grants in these three scope IDs |
| Idempotency conflict | A prior request used that key with a different body |
| Polling ends without success | Read the printed run ID and history; a receipt is not completion |
| You need to call another system | Continue with custom connectors or workers |
The build and simulation steps can be verified offline. Publication, activation, and worker registration require a live, authorized installation; passing the local examples does not establish that your installation has the necessary grants.
What you learned¶
WorkflowBuilderproduces definition data; YAML and Python definitions compile to the same executable digest.WeaveClientcalls the platform:publish,activate, andstart_runeach return the ID the next call needs, and idempotency keys make retries safe.- Custom Python runs in an admitted worker, never inside a workflow definition.
Next, add your own business code with workers, call a REST API without code with the built-in HTTP connector, or plan your product's integration with host integration.