Live Infrastructure Across Europe • Portugal • Finland • Bulgaria
Digital Frontier
Products
DigitalFrontier FlowPreview

Distributed Python pipelines from a decorator

Write distributed pipelines as ordinary Python — one decorator turns an async function into a durable, multi-step pipeline you can publish as an HTTP API.

api.py
python
@app.step
async def double(x: int, services=None):
    return x * 2

@app.step
async def add_ten(x: int, services=None):
    return x + 10

# A workflow composes steps; @app.endpoint publishes it over HTTP
@app.endpoint(path="/run", method="POST")
@app.workflow
async def pipeline(x: int, services=None):
    doubled = await double(x, services=services)
    return await add_ten(doubled, services=services)

DigitalFrontier Flow features

Pipelines are just Python

Mark units of work as steps and compose them into workflows with a decorator; chain them by awaiting one step from another, and express branching, loops and fan-out/fan-in in real code. No YAML DAGs, JSON state machines or DSL to learn, and the same definitions run async or sync.

@app.workflow
async def pipeline(x: int, services=None):
    doubled = await double(x, services=services)
    return await add_ten(doubled, services=services)

Publish a streaming HTTP API

Stack an endpoint decorator on any workflow and the framework generates an OpenAPI schema and typed request/response models from your signature, turning the app into a standard ASGI service. Callers get validated JSON, a job id to poll, and live progress over server-sent events or WebSocket.

@app.endpoint(path="/run", method="POST")
@app.workflow
async def pipeline(x: int, services=None): ...

Warm services & pooled connectors

A service initializes once per worker and stays warm across thousands of tasks, reusing pooled connections and built-in connectors for object storage, databases and popular SaaS tools — expensive setup runs once, not per call.

@app.step(sandboxed=True)   # no filesystem, no direct network
async def run_user_code(x: int, services=None):
    return await services["Database"].query(f"SELECT {x}")

Sandboxed steps for untrusted code

Flip one flag and a step runs inside an isolated WebAssembly sandbox with no host filesystem and no direct network; it reaches your infrastructure only through the specific service methods you approve, so credentials never enter the sandbox. User code is also validated at decoration time and re-verified server-side before it runs.

@method(timeout=30, retries=2, max_concurrency=10,
        rate_limit=10, rate_window=1, rate_key="vendor:default")
async def fetch(self, symbol: str) -> dict: ...

Durable, resumable execution

Every operation is durably recorded, so a worker that restarts mid-task is idempotently re-dispatched and the pipeline finishes instead of failing; abandoned workers are reclaimed on a heartbeat rather than lost.

Full-jitter retries, no storms

Built-in retries use exponential backoff with full jitter, so a downstream blip doesn't become a synchronized retry stampede. Infinite-retry modes are bounded by an attempt cap so a wedged call can't spin forever.

@app.step(gpu="any", gpu_memory="24GB")   # route by memory floor, not a fixed SKU
async def inference(model, services=None): ...

@app.workflow(schedule=Cron("0 9 * * 1-5"))   # server-side, weekdays 09:00
async def daily_digest(sources, services=None): ...

Health-aware regional failover

A stuck unit is detected, cancelled in its old region and re-submitted to the least-loaded healthy region — with an idempotency key appended per attempt so a resubmit can't double-execute. Retries are bounded and non-retryable errors fail fast.

Crash-loop quarantine & respawn

Dead workers are respawned automatically; a worker that keeps crashing is rate-limited and quarantined, so a single poison task can't burn the fleet down in a restart loop.

Fleet-wide rate limits

Cap invocations per sliding window across the whole worker fleet, not per machine — enforced regionally and correct under active-active replication, so you never overshoot a vendor's quota. Denied callers pace themselves instead of parking a worker.

@method(rate_limit=10, rate_window=1, rate_key="vendor:api")
async def fetch(self, symbol: str) -> dict:
    return await self._client.get(symbol)

Distributed concurrency limits

Per-method max-concurrency is enforced across all workers via leased in-flight slots; over-capacity callers wait for a slot rather than being dropped, and a crashed holder's lease expires so capacity is never leaked to zero.

Deadlock-safe deep pipelines

Recursive and nested pipelines raise the warm-worker floor automatically so a deep call chain can't hold-and-wait deadlock, backed by a checked slots ≥ concurrent-roots × depth invariant.

Queue-depth autoscaling & scale-to-zero

Worker pools size themselves from live queue depth, growth rate and the detected bottleneck, and idle per-tenant pools scale to zero after a cooldown — reactivating the moment work arrives, with hysteresis to avoid flapping.

GPUs by type, memory & count

Request accelerators per step by type, a minimum-memory floor, or a GPU count for distributed training; sync GPU work is routed to isolated workers so it can't stall the async event loop.

@app.step(gpu="any", gpu_memory="80GB")
async def infer(model, services=None):
    ...

Micro-batching for inference

Coalesce many individual step calls into one batch — flushing on batch size or a time window, whichever comes first — ideal for GPU inference, with optional padding to a fixed batch size.

Cron & interval scheduling

Schedule any workflow server-side with a standard, timezone-aware cron expression or a fixed interval with jitter — no external scheduler, and exactly one instance fires each occurrence.

@app.workflow(schedule=Cron("0 9 * * 1-5"))   # weekdays 09:00
async def daily_digest(services=None):
    ...

Ephemeral agent workspaces

Give a workflow an isolated, per-tenant workspace to run untrusted or AI-generated code, then throw it away — created, injected as a service, and destroyed around your workflow body by a single decorator.

@app.workflow(workspace={"repo_url": "…", "auto_destroy": True})
async def agent(task, services=None):
    ws = services["workspace"]
    ...

Isolation tier & resource limits

Each workspace gets its own isolation — a userspace-kernel sandbox by default, or an optional microVM tier that gives it a separate kernel — with CPU, memory, PID and disk limits applied per workspace.

Locked-down workspace egress

On a self-hosted cluster, a workspace's outbound traffic is forced through a per-workspace proxy with a domain allow-list, and the network is severed at the install→run boundary so build-time access can't leak into execution.

Thread-persistent agent workspaces

Keep a workspace alive across a conversation: follow-up messages reuse the same sandbox with its cloned repo and state intact, torn down only on a terminal outcome or timeout — built for coding agents.

Get early access to Flow.

Tell us about your workload and we'll get you into the preview.