Aga — durable execution for Python
Write typed functions that survive crashes, restarts, deploys, and long waits.
One App owns declarations, connection, Run creation, and worker lifecycle.
Aga records each durable boundary and reuses its committed result on recovery.
Source-breaking 0.4.2 release. App is now the only home for durable operations. Package-level
join,sleep,signal,event,cancel,info, anddeadlineare removed rather than retained as aliases.
Calls made before a join can overlap in sticky and
distributed workflows; caller waiting does not change placement. concurrency
sizes separate bounded claim and sticky-function pools per worker (default four).
Immediate awaits remain sequential. Checkpoints are written by the workflow
thread; only committed results survive crashes. See the
concurrent-task walkthrough.
Server 0.2.1 or later can inspect measured sticky function times in the dashboard.
Install
Install the SDK with:
python -m pip install --upgrade "aga-runtime==0.4.2"
The distribution is aga-runtime; import it with the short, documented alias:
import aga_runtime as aga
App reads AGA_URL, AGA_TENANT, AGA_NAMESPACE, and AGA_API_KEY by
default; constructor values override the connection location and scope.
Declare durable functions
Declarations belong to one App:
| Annotation | Meaning |
|---|---|
@app.step(...) |
typed durable work plus retry, timeout, and compensation policy |
@app.remote(service, ...) |
typed cross-service or cross-language boundary |
@app.workflow(...) |
durable control flow and worker placement |
@app.schedule(...) |
engine-owned cron creation of root Runs |
from dataclasses import dataclass
import aga_runtime as aga
app = aga.App("checkout")
@dataclass
class Order:
id: str
amount: int
@dataclass
class Receipt:
id: str
approved: bool
@app.step(retry=aga.RetryPolicy(max_attempts=5), pivot=True)
def charge(order: Order) -> Receipt:
return payments.charge(order)
@app.workflow()
async def checkout(order: Order) -> Receipt:
receipt = await charge(order)
if order.amount > 10_000:
await app.signal(
aga.Approval("large-charge", evidence=receipt),
timeout=24 * 60 * 60,
)
app.event("order.charged", {"order": order.id})
return receipt
if __name__ == "__main__":
app.serve()
Aga restores supported declared Python types at durable JSON boundaries, including dataclasses, containers, enums, dates, UUIDs, and Pydantic models.
Create and coordinate work
Calling a registered Step or Remote eagerly creates a typed Handle. Workflows
have one creation form: pass the registered Workflow to app.start(...).
| Method or function | Meaning |
|---|---|
step(args...) |
create Step work inside the active Workflow |
remote(request) |
create cross-service work inside the active Workflow |
app.start(workflow, args...) |
create a root Run outside a Workflow or an owned child Run inside it |
await app.join(*handles, count=None) |
await all, the first, or a threshold |
await app.sleep(seconds) |
park until durable engine time reaches a deadline |
await app.signal(name_or_approval, timeout=...) |
wait for external input or governed approval |
app.event(name, value) |
record an operator-visible milestone |
app.cancel(handle, reason) |
cooperatively cancel durable work |
app.info() |
read immutable current-Run metadata |
app.deadline() |
read the cooperative budget inside this App's Step attempt |
Every Step, Remote, root Run, and child Run uses the same Handle[T]. Inside a
Workflow, await a Handle. Outside, a root Handle also supports blocking
.result(timeout=...).
run = app.start(checkout.options(run_id=order.id), order)
receipt = run.result()
@app.workflow()
async def checkout_with_audit(order: Order) -> Receipt:
audit = app.start(audit_order, order) # owned child; auto-joined
receipt = await charge(order)
await audit
return receipt
Only independent child work opts out of ownership:
app.start(audit_order.options(detached=True), order)
One App normally represents one worker service and may own many Workflow
definitions. Runs have separate execution-local state; they share only the App's
registration, connection, lifecycle, and bounded worker capacity. Import every
module that registers definitions before calling app.serve().
join is the single multi-Handle combinator:
all_values = await app.join(*handles)
first_value = await app.join(*handles, count=1)
two_values = await app.join(*handles, count=2)
Sync, Async, and Async Distributed
These describe two separate choices:
- Sync versus Async is caller behavior. Sync waits for a root Handle's result; Async keeps the same Handle and lets the caller continue.
- Async versus Async Distributed is workflow placement. Default Async keeps ordinary Steps with the Workflow worker. Async Distributed independently dispatches and fences each Step.
Use Sync when a request or script needs the result now. Use Async for long work
or responsive callers. Use Async Distributed when Steps need separate scaling,
isolation, placement, or leases. There is no execution="sync"; waiting never
changes placement.
# Sync caller
receipt = app.start(checkout, order).result()
# Async caller
run = app.start(checkout, order)
receipt = await run
# Distributed placement
@app.workflow(execution="async_distributed")
async def distributed_checkout(order: Order) -> Receipt:
return await charge(order)
A caller timeout does not cancel the durable Run. Cancellation is explicit with
app.cancel(run, reason).
Waiting, services, and schedules
sleep completes through engine time. signal completes through external
input. Approval is an immutable request passed to signal, not a third wait
verb; it selects governed, fail-closed behavior and replay-bound evidence.
A Remote accepts one required request object so its envelope maps to Python, Go, Java, and TypeScript without language-specific argument rules. The serving App registers a Step with the same method name and portable request schema.
Schedules are engine-owned root-Run creation and continue while workers are offline:
@app.schedule("0 6 * * *", input={"report": "kpi"}, revision=2)
@app.workflow(name="reports.daily")
async def reports_daily(request: dict[str, str]) -> Report:
return await build_report(request["report"])
Migrating from 0.4.1
| Python 0.4.1 | Python 0.4.2 |
|---|---|
aga.join(...) |
app.join(...) |
aga.sleep(...) |
app.sleep(...) |
aga.signal(...) |
app.signal(...) |
aga.event(...) |
app.event(...) |
aga.cancel(...) |
app.cancel(...) |
aga.info() |
app.info() |
aga.deadline() |
app.deadline() |
There is no compatibility alias. Migrate application code and tests together, then restart the 0.4.2 worker fleet as one coordinated change. Aga server 0.2.1 remains compatible; this SDK release does not change the wire or schema.
Documentation: overview · Python guide · source
Apache License 2.0. See LICENSE.
Metadata
Release files for aga-runtime 0.4.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| aga_runtime-0.4.2.tar.gz | 67.9 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| aga_runtime-0.4.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 165.9 kB
Release files / aga_runtime-0.4.2.tar.gz
| Download URL | aga_runtime-0.4.2.tar.gz |
|---|---|
| Size | 67.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4d8c8f51355660d98d49c2970763030b48ddf5f8fb2f200215c1bc62e946bf7f
|
|
BLAKE2b-256 checksum How to use checksums |
15b1ec2e692c357a3e4ea146ac920030f86911fee5c201aa723ad95a4eb9d76f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.25
|
Release files / aga_runtime-0.4.2-py3-none-any.whl
| Download URL | aga_runtime-0.4.2-py3-none-any.whl |
|---|---|
| Size | 98.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
75aad00fee9839cf96353df42bdd8837423ba8bba8b9e1fb2eb087aadbfc14e5
|
|
BLAKE2b-256 checksum How to use checksums |
a4d76c933db60410b98dfb1ded5607680de5d24ca81c47345a1328a9651c57a1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.25
|
Security for aga-runtime 0.4.2
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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.