Build with the local Soterias SDK. Get started with Python and TypeScript.
SoteriasDocsBuild
Workflows

Personal and enterprise agent teams

Build a collector, analyst, independent reviewer and coordinator with explicit data handoffs. These examples use deterministic role functions and synthetic local data, so you can run them before adding a model or credentials. Replace a role with your existing agent while keeping its input/output contract.

Workflow What the team produces Next protected action
Personal travel A rail itinerary within the budget Booking and payment
Personal inbox A review queue containing M1 Draft creation or sending
Enterprise procurement Approved vendor V1 Purchase-order submission
Enterprise incident response The API service investigation Restart or deployment
AML review Accounts A01 and A03, totaling 467500 cents Case disposition

Install the matching API preview

These examples use the API 2 preview, not names promised by the public RC5 release. Existing RC5 entry points remain available. The Python wheel below is verified on Windows x64; it is not a cross-platform release.

shell
python -m pip install https://docs.soterias.io/downloads/api-preview/soterias_local-0.1.0rc5%2Bapi2-cp311-abi3-win_amd64.whl
shell
npm install https://docs.soterias.io/downloads/api-preview/soterias-core-0.1.0-rc.5-api.2.tgz
npm install --save-dev typescript@5.8.3 @types/node@24

Python imports soterias. TypeScript imports @soterias/core. Use Python 3.11+ for this wheel and Node 22.23.4+ for these examples. For a .mts file, compile with npx tsc example.mts --target ES2022 --module NodeNext --moduleResolution NodeNext --skipLibCheck, then run the emitted .mjs file.

Run a team

Save the Python code as team.py, or the TypeScript code as team.mts. Use a new local output directory:

shell
python team.py travel travel-review
shell
npx tsc team.mts --target ES2022 --module NodeNext --moduleResolution NodeNext --skipLibCheck
node team.mjs travel travel-review-ts

Replace travel with inbox, procurement, incident or aml. No trusted-host provisioning is needed for this analysis example. The code preserves original values, independently verifies cache reuse and writes JSON/CSV/OTLP observations locally.

Application walkthrough

Choose Python or TypeScript. Create team.py or team.mts and append that language's chunks in order. Each language forms one complete program; indentation and open blocks carry across chunks.

Import the packages

Import the SDK and standard-library helpers. These are the real package exports used by the executable program.

"""Runnable synthetic teams. No model, account or external effect required.

python team_workflows.py aml NEW_DIRECTORY
Also: travel, inbox, procurement, incident. These role functions are application
agents, not separate security principals. Ordinary observation enforces no policy.
"""

import argparse
import asyncio
import json
from pathlib import Path
from soterias import Soterias, canonicalize, canonical_digest

Collect versioned inputs

Each role receives explicit data, rather than hidden shared state.

# %% Collect versioned inputs
# Each role receives explicit data, rather than hidden shared state.
def collect(kind):
    """Collector agent: return synthetic source data with an explicit version."""
    datasets = {
        "aml": {
            "transactions": [
                {"account": "A01", "cents": 125000},
                {"account": "A01", "cents": 87500},
                {"account": "A02", "cents": 12000},
                {"account": "A03", "cents": 255000},
            ],
            "threshold": 200000,
        },
        "travel": {
            "options": [{"name": "Rail", "cost": 90}, {"name": "Flight", "cost": 240}],
            "budget": 150,
        },
        "inbox": {
            "messages": [{"id": "M1", "urgent": True}, {"id": "M2", "urgent": False}]
        },
        "procurement": {
            "quotes": [
                {"vendor": "V1", "cost": 700, "approved": True},
                {"vendor": "V2", "cost": 500, "approved": False},
            ],
            "budget": 800,
        },
        "incident": {
            "signals": [
                {"service": "api", "errors": 12},
                {"service": "worker", "errors": 0},
            ]
        },
    }
    return {"kind": kind, "version": "synthetic-v1", "data": datasets[kind]}

Analyse the data

This deterministic analyst can be replaced with your existing agent.

# %% Analyse the data
# This deterministic analyst can be replaced with your existing agent.
def analyse(source):
    """Analyst agent: calculate a proposal. It cannot approve or execute it."""
    data, kind = source["data"], source["kind"]
    if kind == "aml":
        totals = {}
        for row in data["transactions"]:
            totals[row["account"]] = totals.get(row["account"], 0) + row["cents"]
        return {
            "review_accounts": sorted(
                k for k, v in totals.items() if v >= data["threshold"]
            ),
            "review_total_cents": sum(
                v for v in totals.values() if v >= data["threshold"]
            ),
        }
    if kind == "travel":
        return {
            "shortlist": [
                x["name"] for x in data["options"] if x["cost"] <= data["budget"]
            ]
        }
    if kind == "inbox":
        return {"review_messages": [x["id"] for x in data["messages"] if x["urgent"]]}
    if kind == "procurement":
        return {
            "shortlist": [
                x["vendor"]
                for x in data["quotes"]
                if x["approved"] and x["cost"] <= data["budget"]
            ]
        }
    return {"investigate": [x["service"] for x in data["signals"] if x["errors"] > 0]}

Verify the proposal independently

Recompute the business criterion; returning a value does not prove task quality.

# %% Verify the proposal independently
# Recompute the business criterion; returning a value does not prove task quality.
def verify(source, proposal):
    """Independent reviewer agent. It returns evidence, never signed authority."""
    if source["kind"] == "aml":
        rows, threshold = source["data"]["transactions"], source["data"]["threshold"]
        accounts = sorted({r["account"] for r in rows})
        flagged = [
            a
            for a in accounts
            if sum(r["cents"] for r in rows if r["account"] == a) >= threshold
        ]
        return proposal == {
            "review_accounts": flagged,
            "review_total_cents": sum(
                r["cents"] for r in rows if r["account"] in flagged
            ),
        }
    expected = {
        "travel": {"shortlist": ["Rail"]},
        "inbox": {"review_messages": ["M1"]},
        "procurement": {"shortlist": ["V1"]},
        "incident": {"investigate": ["api"]},
    }
    return proposal == expected[source["kind"]]

Coordinate and observe the team

Use the same SDK client around the existing role calls and export local observations.

Python
# %% Coordinate and observe the team
# Use the same SDK client around the existing role calls and export local observations.
async def run(kind, directory):
    directory.mkdir(mode=0o700)  # Refuse an existing output directory.
    sdk = Soterias(workflow="team-" + kind, directory=directory / "metrics")
    cached = sdk.cache(analyse, version="team-analysis-v1", verify=verify)
    try:
        # Coordinator orchestrates role boundaries; no provider credentials are needed.
        source = sdk.invoke(collect, kind)
        proposal = sdk.invoke(cached, source)
        repeated = sdk.invoke(cached, source)
        assert repeated == proposal and sdk.invoke(verify, source, proposal)
        assert cached.cache_stats()["caller_accepted_hits"] == "1"
        record = canonicalize(
            json.dumps({"source": source, "proposal": proposal}).encode()
        )
        (directory / "proposal.json").write_bytes(record)
        # Local application output only: this is not a broker-approved case closure.
        print(
            json.dumps(
                {
                    "workflow": kind,
                    "proposal": proposal,
                    "digest": canonical_digest(record),
                    "security_enforcement": False,
                    "external_effects": False,
                }
            )
        )
        await sdk.flush()
        for suffix, export in (
            ("json", sdk.metrics.export_json),
            ("csv", sdk.metrics.export_csv),
            ("otlp.json", sdk.metrics.export_otlp),
        ):
            page, offset, number = None, 0, 0
            while True:
                page = export(offset) if suffix != "otlp.json" else export()
                (directory / f"metrics-{number}.{suffix}").write_bytes(page.data)
                if page.next_offset is None:
                    break
                offset, number = page.next_offset, number + 1
        print(json.dumps(sdk.metrics.snapshot().data()))
    finally:
        cached.close()
        await sdk.close_async()


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "workflow", choices=("aml", "travel", "inbox", "procurement", "incident")
    )
    parser.add_argument("directory", type=Path)
    args = parser.parse_args()
    asyncio.run(run(args.workflow, args.directory.resolve()))

Protect the final action

Use protected execution when an action needs enforced authority. The analyst proposes data; the trusted executor, separate reviewer and native broker own admission. The AML workflow shows original receipt verification and SQLite readback.

Search guides, examples and API reference.