grida
The Python SDK for Grida FX. Import it as grida.fx.
It does three things: it authors node types and workflows (node, Workflow), it hosts node
bodies when the engine runs them (python -m grida.fx.host, started by the engine), and it drives
the grida-fx engine from Python. The engine is the only place with engine logic: planning,
identities, the cache and budgets all happen in grida-fx, never in this package.
Install
pip install grida
Python releases use stable versions starting at 0.1.0 (uv pip install grida also works).
Each wheel carries the engine for its platform: macOS on Apple silicon (11 or later) and
on Intel (10.12 or later), and Linux with glibc 2.28 or later on x86_64 and aarch64. Windows is not
supported: the engine's runner uses Unix process groups and signals. There is no source
distribution, and the wheels install no console command: the engine runs as python -m grida.fx.
From a checkout of the repository, python3 tools/build_engine.py builds the engine and puts it
where a wheel carries it: python/src/grida/fx/_bin/grida-fx in the checkout (ignored by git). An
editable install of the checkout's python/ folder (uv sync --project python, or a path
dependency in another project) then finds it. Run it again after every pull.
The engine from the command line
python -m grida.fx plan workflows/gallery.yaml --poster inputs/poster.png
python -m grida.fx run workflows/gallery.yaml --poster inputs/poster.png --live --max-usd 10
python -m grida.fx <verb> … takes the same arguments as grida-fx and exits with its status
(on POSIX it becomes the grida-fx process), so a Python installation needs no Node. The binary
is the first of:
GRIDA_FX_BIN, when set (a path; relative to the current directory);- the one inside the package (
grida/fx/_bin/grida-fx): shipped in the wheel, or built into a checkout bytools/build_engine.py; grida-fxonPATH.
With none of them, the command prints grida-fx: … naming GRIDA_FX_BIN and exits 2.
Planning and running from Python
from grida.fx import plan, run
planned = plan("workflows/gallery.yaml", inputs={"poster": "inputs/poster.png"}, max_usd=10)
print(planned.ok, planned.problems, planned.estimate()) # estimate: (low, high) in US dollars
for phase in planned.phases():
print(phase["phase"], phase["steps"], phase["high_usd"])
result = run(planned, live=True) # runs the plan with the target and options it was planned with
print(result.ok, result.cost, result.incomplete, result.failed)
for key, image in result.outputs["images"].items(): # a keyed collection
verdict = result.steps[f"entity['{key}'].review"].facts["verdict"]
print(key, image.path, verdict)
missing = result.deliver({"images": "out/{key}.png"})
- Targets. A workflow file, a workflow id, a builder
file.py:function(its arguments asarguments={"level": "docks"}, the command line's--arg), or aPlan. AWorkflowobject is refused withTypeError: name the builder that returns it. - Options.
inputs(a mapping, written to a temporary inputs file incwd, so relative paths in it mean what they mean on the command line),input_files(--inputs),routes(--routes),max_usd(--max-usd);runaddslive,yes_up_toand one ofname(create-only),resume(require-existing) orrun_dir(explicit-folder create/resume).cwdis the engine's working directory (default: the current one); every relative path is relative to it. APlantakes no planning options when it runs: plan again to change them. planasks the engine for the plan (grida-fx plan --json) and its price (grida-fx price).Plan.documentis the fx-graph-v1 document;ok,problems(where,message),estimate()andphases()read it. Planning never spends.runplans first, and raisesPlanRefused(the plan is refused:and one line per problem) before any run folder exists. A failed step does not raise: checkresult.okandresult.failed. The result is read from the run folder'sevents.jsonl:ok,incomplete,cost(the folder's charge over every invocation, in US dollars),run_dir,failed,failures,stopped,stand_in,outputsandsteps.- Failures.
result.failuresholds aFailurefor each failed instance id: itspath,message,code(the protocol's name of the error, such asnode_failureorcall_failed;Nonefor a timeout, an assertion or a skip), the nodefactsit reported, andskipped(it never ran, because something it reads failed).result.stoppedsays why a run stopped early. - Files. An output file has
path(its copy in the planning project's store,<cache>/files/…: the project above the workflow file for a.yamltarget, else abovecwd; read it, never write it),digest,kind,name,size,key,read_bytes()andcopy_to(). A keyed collection is adict, a list alist, an absent outputNone. - Delivering.
result.deliver({output: path})copies output files out of the run;{key}in the path names each element of a collection or list (keys are made safe as run folders make them). A file that already holds the same bytes is left alone. It returns the outputs that hold no file. - Errors. The engine's own errors (exit status 2: a workflow it cannot read, a bad option)
raise
FxErrorwith its message; so does a run it refuses to start (refused: …). - Async.
plan_asyncandrun_asyncdo the same inside an event loop;planandruncannot be called inside a running one. Cancellingrun_asyncrequests graceful engine cancellation once with SIGTERM and waits for cleanup before unwinding; repeated task cancellation does not force the engine. A stuck cleanup can keep the task pending.
Saved runs and observation
load_run(target, verify=False, cwd=None) returns a RunRecord containing the
engine's inspection and absolute run_dir. It accepts a folder, workflow/name,
or workflow ID for its newest-created run. The selected folder stays pinned even
if a newer run appears. It has no invocation exit code.
Use record.snapshot() for a consistent plan/event prefix and
record.events(after=cursor, limit=256) for bounded reads. Their async forms are
snapshot_async and events_async; loading has load_run_async. Documents retain
wire field names. record.follow(after=cursor, poll_interval=1.0) is an async
generator of nonempty batches, continuing through completion and later resumes
until broken or cancelled. Observation cancellation never cancels execution.
Reader failures raise RunObservationError with code and document; malformed
responses raise FxError. Snapshot and inspection are separate samples. See
Working with runs from an SDK for cursor-safe
attachment, named execution/resume and reader lifetime. This describes current
source, not a guarantee that an older published package has these APIs.
Run control
inspect_control(run) and cancel(run, invocation=None, wait=False, timeout=None)
inspect or cancel an exact run folder or WORKFLOW_ID/NAME. Their async counterparts
are inspect_control_async and cancel_async. RunControlResult preserves the wire
contract's lower_snake_case fields. Structured failures raise RunControlError
with code and result; unsupported binaries or malformed responses raise FxError.
Choose run_dir to control a pending SDK invocation through these separate operations.
See Stopping and continuing a run for guarded requests, wait timeouts and verified local completion. Local completion never verifies remote provider completion. These APIs describe current source; check your installed version.
Listing and removing runs
list_runs(workflow=None, cwd=None) returns the planning project's runs, newest first, as
RunEntry objects with the wire contract's fields and run_dir, the folder resolved against
cwd. remove_runs(runs, preview=False, cwd=None) removes exactly the named runs (folders,
run_dir values or WORKFLOW_ID/NAME); preview=True only reports what would go. There are
no filters: choose from the list yourself. The async forms are list_runs_async and
remove_runs_async. A run that is active or the last holding a pick is refused in the
RunRemoval result, not raised; a target that is not a run raises RunRemovalError with
code and run, and nothing is removed. An applied removal whose result cannot be read
raises FxError saying it may already have taken effect, with the engine's stdout.
Stand-ins: running without a provider
A test runs a workflow's paid calls offline by answering them itself:
from collections import Counter
from pathlib import Path
from grida.fx import DECLINE, Answer, CallRefused, run
asked = Counter()
def answer(call): # a plain function, or an async def
asked[call.capability] += 1
if call.capability == "image.generate":
return Answer(files={"image": Path(f"tests/pictures/{call.request['size']}.png")})
if call.capability == "structured.generate":
return Answer.json({"title": "The lighthouse"})
if call.capability == "agent.turn":
return Answer.submit(caption="a lighthouse at dusk")
if call.capability == "image.edit":
raise CallRefused("no edits in this test")
return DECLINE
result = run("workflows/gallery.yaml", run_dir="runs/test", stand_in=answer)
assert asked["image.generate"] == 2
assert result.failures["touch_up#1"].code == "capability_refused"
- The call.
answergets aStandInCallfor each paid call the cache cannot answer:capability,route.idandroute.fingerprint,key,takesandtake,instance.id,.pathand.step,request(every file in it anInputFile:digest,path,read_bytes(),facts),files(the same files by digest) andparams(the call as the engine sent it, files as{"file": digest}). - The answer.
Answer(files={name: …}, data=…): a file isbytes(it takes the kind the capability names), anOutputof bytes or anInputFile(its own kind), or a path (the kind of its suffix).Answer.json(value),Answer.turn(text, tool_calls)andAnswer.submit(**arguments)build the data ofstructured.generateandagent.turn. ReturnDECLINEto leave a call unanswered (it failsnot_live); raiseCallRefusedorCallFailedto refuse or fail it. The engine checks every answer as it checks a provider's: an image of the wrong size fails the call. - One at a time. The function runs in this process, so counters and scripted answers work. Calls are answered one at a time, in the order they come.
- Faults. Anything else it raises, or a return of
None, stops the run;runthen raises that exception, noting the run folder. - Nothing is spent, and the real store is left alone. A stand-in run is never
liveand takes noyes_up_to;rundoes not plan it first. It keeps everything in the store'sstand-in/folder, where its result's files are read, so a later stand-in run, or a resumed one, replays its answers; delete that folder to forget them. A folder resumes only in the mode it was started in, andrerollandpickrefuse it. - From the command line:
grida-fx run … --stand-in tests/stand_in.py#answer.
Standard bodies
grida.fx.std holds the Python bodies of FX's free standard node types. The engine declares
them, and runs them in this package's node host when a workflow uses one (fx/image.resize@1,
…); nobody imports them to use them.
| Type | What it does |
|---|---|
fx/image.mirror_repeat@1 |
mirrors a picture onto itself on x, y or both, at most 16384 px a side |
fx/image.check_alpha@1 |
judge: transparent wants a fully transparent pixel, anything else every pixel opaque |
fx/image.check_size@1 |
judge: the picture's width and height against the ones asked for |
fx/image.resize@1 |
longest_side, or width and height; Lanczos |
fx/image.crop@1 |
a box in fractions, or the first box mark of a region, grown by padding |
fx/image.pad@1 |
centres a picture on a transparent canvas |
fx/json.merge@1 |
a shallow merge of JSON objects, a later key winning |
fx/files.copy@1 |
the same bytes and kind |
fx/package@1 |
lays files out by destination path ({key} takes a keyed collection) with a manifest |
The bodies are ported byte for byte from FX's predecessor, and every picture they write comes
from Pillow's own PNG encoder, so pillow is pinned to one exact version: another build may write
other bytes, which would change every identity and request downstream of them.
grida.fx.std.body_of("fx/<name>@1") returns a body; grida.fx.std.pictures holds the Pillow
reads and writes behind ctx.read.image and ctx.out.png.
Metadata
Release files for grida 0.5.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| grida-0.5.1-py3-none-manylinux_2_28_x86_64.whl | Python 3 | none | Linux glibc 2.28+ x86-64 | Details |
| grida-0.5.1-py3-none-manylinux_2_28_aarch64.whl | Python 3 | none | Linux glibc 2.28+ ARM64 | Details |
| grida-0.5.1-py3-none-macosx_11_0_arm64.whl | Python 3 | none | macOS 11.0+ ARM64 | Details |
| grida-0.5.1-py3-none-macosx_10_12_x86_64.whl | Python 3 | none | macOS 10.12+ x86-64 | Details |
Total release size: 33.8 MB
Release files / grida-0.5.1-py3-none-manylinux_2_28_x86_64.whl
| Download URL | grida-0.5.1-py3-none-manylinux_2_28_x86_64.whl |
|---|---|
| Size | 8.9 MB |
| Tags | Linux glibc 2.28+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
7076f431f29c285f0c8e76e7c429e857048136c58bac12e01ffbb01b7897063d
|
|
BLAKE2b-256 checksum How to use checksums |
9d2d66cb83f916b19b3037f88b8cf64608b2745a8270456a6fc0bdcd5eb49824
|
| 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 12, 2026.
Transparency logRelease files / grida-0.5.1-py3-none-manylinux_2_28_aarch64.whl
| Download URL | grida-0.5.1-py3-none-manylinux_2_28_aarch64.whl |
|---|---|
| Size | 8.3 MB |
| Tags | Linux glibc 2.28+ ARM64 Python 3 |
|
SHA-256 checksum How to use checksums |
a167a7a700bf05bf2e39964cec08db2dade8e9aaae7874b07912fd1f9e33b855
|
|
BLAKE2b-256 checksum How to use checksums |
b0d6f2aa7355e91f37a9b0e3f4fab1f0c3765e7189e427b4cc3310e9621a9559
|
| 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 12, 2026.
Transparency logRelease files / grida-0.5.1-py3-none-macosx_11_0_arm64.whl
| Download URL | grida-0.5.1-py3-none-macosx_11_0_arm64.whl |
|---|---|
| Size | 8.1 MB |
| Tags | Python 3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
bd4bb4b39ccc492176c76b0fc96328242a4d8ab31bf4904023a3cdd2e743ba89
|
|
BLAKE2b-256 checksum How to use checksums |
c543c732358f0b1e17d44c8946be86fe6ef2c0017ef0330e6110e0101741b79a
|
| 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 12, 2026.
Transparency logRelease files / grida-0.5.1-py3-none-macosx_10_12_x86_64.whl
| Download URL | grida-0.5.1-py3-none-macosx_10_12_x86_64.whl |
|---|---|
| Size | 8.6 MB |
| Tags | Python 3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
ee12dc4ed426b73c776d207a651265edacfc82db16eb0c644f5188c053361bba
|
|
BLAKE2b-256 checksum How to use checksums |
67f59ac60d3a5ff5a45d3d7490ebf628579d665a4f4dab4132ced55eae0417e8
|
| 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 12, 2026.
Transparency log