Durable workflows. Durable agents.
Flux is a distributed workflow engine for Python, with a first-class agent framework built in. Async-native. Type-safe. Multi-provider. Build agents that pause, resume, retry, and replay like the workflows they actually are.
Most engines pick one or two. Flux gives you all three, by default.
Workflows that remember.
Every step is checkpointed. Every event is replayable. Pause a workflow for a human approval today, resume it tomorrow with new input. State is durable across restarts, deploys, and failures — and when a hot path doesn't need replay, mark it transient and skip the bookkeeping.
Scale without rewriting.
Run locally as a script, then drop in workers when you need to scale. One Python file, n servers, n workers, coordinated over Server-Sent Events and PostgreSQL with resource-aware task assignment.
Built to recover.
Retries with exponential backoff. Fallbacks. Rollbacks. Timeouts that fire. Cancellation that propagates. The same machinery that makes a payment workflow durable makes an AI agent survive a flaky LLM call.
AI agents that don't lose their place.
Most agent frameworks treat durability as an afterthought. Flux makes it the foundation. Pause for human approval. Resume after a crash. Replay deterministically to debug. Your agent inherits all of it, because your agent is a workflow.
One primitive. Every pattern.
The agent() primitive returns a Flux task that calls an LLM. Add tools. Add skills. Add memory. Delegate to sub-agents. Stream tokens. Get structured output.
Every option is composable. Every call is checkpointed. Switch from GPT-4o to Claude to a local Ollama model by changing one string. The rest of your code doesn't move.
# The full agent() signature — every capability declared in one place. from flux.tasks.ai import agent assistant = await agent( system_prompt="You are a thoughtful research assistant.", model="anthropic/claude-sonnet-4-5", # Capabilities — all optional, all composable tools=[search, fetch_page, summarize], skills=skill_catalog, # SKILL.md files (agentskills.io) agents=[fact_checker, editor], # sub-agent delegation # Reasoning & planning planning=True, max_plan_steps=15, approve_plan=True, # human approves the plan first reasoning_effort="medium", # Memory working_memory=working_memory(), long_term_memory=long_term_memory(provider=sqlite("mem.db")), # Output & streaming response_format=Briefing, # Pydantic typed output stream=True, # Safety rails max_tool_calls=20, max_tokens=4096, # Lifecycle hooks on_complete=[dream], # consolidate memory in background on_pause=[notify_user], )
Everything an agent needs. Nothing it doesn't.
Any task is a tool
Annotate a Python function with @task, hand it to agent(tools=[...]), and the LLM can call it. Tool calls are checkpointed events: replay them, retry them, audit them.
tools=[search_web, fetch_page,
send_email, deploy_pr] SKILL.md, discovered
Implements the agentskills.io open standard. Drop SKILL.md files in a directory; the catalog discovers them at runtime. The LLM picks which to activate per task.
SkillCatalog.from_directory(
"./skills"
) Working & long-term
Working memory holds the live conversation and tool results. Long-term memory persists across sessions in SQLite or Postgres, scoped per user, per agent, per anything.
working_memory=working_memory(), long_term_memory=long_term_memory( provider=sqlite("mem.db"), scope="user:alice", )
Background consolidation
A four-phase workflow (orient, gather signal, consolidate, prune) that runs after the agent finishes. Distills working memory into clean long-term knowledge while you're not watching.
from flux.tasks.ai.dreaming import dream agent(..., on_complete=[dream])
Plan, approve, execute
Set planning=True and the agent drafts an explicit plan before acting. Optionally pause for human approval. Replan dynamically on failure.
planning=True, approve_plan=True, max_plan_steps=15,
Sub-agents as tools
Pass other agents into the agents= parameter. A lead agent can delegate to specialists, and each delegation runs as its own workflow execution with its own tools and prompt.
lead = await agent( ..., agents=[researcher, coder], )
Tokens in real time
Stream LLM tokens to the client as they arrive over Server-Sent Events. State checkpoints don't block the stream, so you get observability and durability at the same time.
agent(..., stream=True) Typed responses
Pass a Pydantic model to response_format= and the agent returns instances of that type. No regex parsing. No JSON repair loops.
class Briefing(BaseModel): summary: str citations: list[str] agent(..., response_format=Briefing)
Tool discovery via protocol
Connect to any MCP server and the agent picks up its tools dynamically. Bidirectional: Flux can host an MCP server too, exposing your workflows to other agents.
mcp_servers=[
{"url": "https://mcp.github.io",
"auth": "bearer"}
] name: coder model: anthropic/claude-sonnet-4-5 description: Senior engineer with workspace access system_prompt: | You are a senior software engineer. Use the available tools to read, search, and edit files. Run the test suite after changes. tools: - system_tools: workspace: . timeout: 60 mcp_servers: - url: https://mcp.github.example.com auth: bearer secret: GITHUB_TOKEN planning: true max_plan_steps: 15 max_tool_calls: 30 reasoning_effort: medium stream: true
# Create the agent $ flux agent create coder \ --file coder.yaml # Start it — terminal, API, or web UI $ flux agent start coder \ --mode terminal # …or expose it as an HTTP API $ flux agent start coder \ --mode api --port 9100 # Compose with other agents $ flux agent create lead \ --file delegation.yaml # lead.yaml lists "agents: [coder, researcher]" # the lead delegates each sub-task to the right specialist
Server, workers, state. Open at every layer.
Flux is a small set of cooperating processes connected over standard protocols. Run it on your laptop. Run it across a cluster. The shape of the system stays the same.
However you reach it.
- CLIWorkflow, schedule, agent, secret, worker management. All from
flux .... - REST APIFastAPI under the hood. Swagger UI at
/docs. Anything that speaks HTTP works. - MCP serverModel Context Protocol. Expose workflows to AI agents over SSE, http, or stdio.
- Python SDKRun workflows in-process for tests and scripts. No server required.
The coordinator.
- Workflow catalogRegister Python files; the server tracks every workflow, its version, its schema.
- SchedulingCron, interval, one-time. Auto-created when workflows register. Pause, resume, modify.
- DispatchingMatches executions to workers on resources, labels, and free capacity slots. Polling or event-driven, one config key.
- Worker registryBootstrap-token auth. Live tracking of CPU/memory/GPU. Session management.
- ReplicatedRun one server or several. Replicas coordinate through PostgreSQL — no leader election, no extra moving parts.
- Secret managementEncrypted vault with per-task
secret_requestsinjection. - Workflows as servicesEvery registered workflow is automatically a REST endpoint and an MCP tool.
Where the work runs.
- Auto-registeringBoot, present a token, get a session. Reconnect on network loss.
- Resource-awareReport CPU, memory, GPU, and concurrency slots. Server matches load to capacity.
- SSE-drivenReceive scheduled, resumed, and cancelled events as they happen.
- Isolated runsEach execution gets its own credential-less subprocess by default — or run in-process, or in Docker.
- CheckpointingSend execution state back to the server after each step.
- Horizontally scaledAdd more, anywhere. Drain gracefully on SIGTERM — finish the work, flush state, exit.
The single source of truth.
- EventsEvery state change: task started, retried, cached, paused, completed.
- ArtifactsTask inputs, outputs, intermediate results. Addressable, replayable, cacheable.
- CheckpointsResume any execution from its last good state. Survive crashes and restarts.
- Replayable historyWalk through a past execution event-by-event. Deterministic debug. Retention sweeps are one config away.
- Pluggable backendSQLite for local. PostgreSQL for clusters, with schema migrations managed for you. Same API.
Multiple identity providers.
Use your existing IdP via OpenID Connect, or issue API keys for service accounts. Discovery, JWKS rotation, and clock-skew tolerance all built in.
- OIDC with JWT validation
- API keys (SHA-256 hashed)
- Bootstrap tokens for workers
- Execution tokens, HMAC-signed
RBAC with wildcard permissions.
Permissions are colon-separated and support wildcards at any segment. Built-in roles cover common cases; custom roles compose freely.
workflow:*:*:runexecution:*:read- Roles: admin · operator · viewer · worker
- Per-task permission tree
AES-256-GCM, with PBKDF2.
Sensitive fields are encrypted before they touch the database. Authenticated encryption ensures tampering is detectable, not just decryption-blocking.
- AES-256-GCM cipher
- PBKDF2 · 1M iterations · SHA-256
- Per-record 32-byte salt
- Per-task
secret_requestsinjection
Service accounts and users.
A unified principal registry tracks who's who, what they can do, and when they last connected. Disable access without deleting history.
- Service accounts & user identities
- External issuer binding (OIDC)
- Last-seen tracking
- Enable / disable, never delete
Skip the platform engineering. It's already in here.
Retries with backoff
Configurable max attempts, initial delay, exponential multiplier, per task.
Timeouts that fire
Hard cutoffs per task. Hung HTTP calls, runaway loops, deadlocks: all bounded.
Fallback & rollback
Define an alternate path on failure. Or compensate: close the file, refund the charge.
Authentication, pluggable
OIDC for humans, API keys for services, execution tokens for workflows. JWT-validated, SHA-256-hashed, expirable.
RBAC with wildcards
Colon-separated permissions like workflow:*:*:run. Built-in roles: admin, operator, viewer, worker.
Encryption at rest
AES-256-GCM with PBKDF2 (1M iterations, SHA-256). Per-record salt. Authenticated, so tampering is detectable, not just decryption-resistant.
Auto-scheduling
Cron, interval, one-time. Schedules created when workflows register. Pause, resume, modify.
Pause & resume
Wait for an external trigger, an input, an upstream signal. Resume with new data, minutes or months later.
Graceful cancellation
Cancel running workflows. CancelledError propagates so rollback handlers run.
Task caching
Set cache=True. Identical inputs return memoized results: expensive computation runs once.
Deterministic replay
Every event is recorded. Replay a workflow event-by-event to debug exactly what happened. No “works on my machine”.
Durable — or transient
Checkpoint every step by default. Or mark a workflow durability="transient" and skip the bookkeeping when speed beats replay.
Resource-aware workers
Workers report CPU, memory, GPU, free slots, and live metrics. Tasks declare requirements. Scoring policies rank the eligible workers — or the server just picks the least loaded.
Subworkflows
Workflows can call other workflows. Each gets its own execution, its own state, its own retries. Composition without coupling.
Sync, async, or stream
Run a workflow and wait. Or fire-and-forget. Or subscribe to its events over SSE. Same workflow, three execution modes.
Built-in tasks
parallel, pipeline, Graph, sleep, pause, now, uuid4, choice. The orchestration primitives, batteries included.
Type-safe by default
ExecutionContext[T] is generic. Type hints on tasks. Pyright-checked. Errors caught before they ship.
FastAPI included
REST API for workflows, schedules, secrets, workers. Swagger UI at /docs. Plug it into anything that speaks HTTP.
Decorators in. Reliability out.
Flux is the workflow engine that gets out of your way. Two decorators, full type hints, async/await throughout. Then everything you'd expect from a durable runtime, for free.
# A real agent loop. Multi-turn conversation that survives restarts. # Pattern from examples/ai/conversational_agent_anthropic.py from flux import task, workflow, ExecutionContext from flux.tasks import pause from anthropic import AsyncAnthropic @task.with_options( secret_requests=["ANTHROPIC_API_KEY"], retry_max_attempts=3, retry_backoff=2, timeout=60, ) async def conversation_turn(messages, user_message, system_prompt, secrets={}): """One turn: append user message, call Claude, append response.""" messages.append({"role": "user", "content": user_message}) client = AsyncAnthropic(api_key=secrets["ANTHROPIC_API_KEY"]) response = await client.messages.create( model="claude-sonnet-4-5-20250929", max_tokens=1024, system=system_prompt, messages=messages, ) reply = response.content[0].text messages.append({"role": "assistant", "content": reply}) return messages, reply @workflow async def conversational_agent(ctx: ExecutionContext[dict]): initial = ctx.input or {} system_prompt = initial.get("system_prompt", "You are a helpful assistant.") max_turns = initial.get("max_turns", 10) messages = [] messages, _ = await conversation_turn(messages, initial["message"], system_prompt) # The loop: pause for the next user message, run the turn, repeat. # Each pause is a durable checkpoint — resume tomorrow if you want. for turn in range(1, max_turns): resume_input = await pause(f"awaiting_user_input_turn_{turn}") next_message = (resume_input or {}).get("message") if not next_message: break messages, _ = await conversation_turn(messages, next_message, system_prompt) return {"history": messages, "turns": len(messages) // 2}
# Any @task becomes a tool. The agent decides when to call it. # Tool calls are checkpointed — debug-replay them deterministically. from flux import task, workflow, ExecutionContext from flux.tasks.ai import agent from pydantic import BaseModel @task async def get_weather(city: str) -> dict: """Fetch current weather for a city.""" return await open_meteo.fetch(city) @task async def get_forecast(city: str, days: int = 3) -> list: """Multi-day forecast for a city.""" return await open_meteo.forecast(city, days) class Forecast(BaseModel): summary: str high_c: float advice: str @workflow async def weather_briefing(ctx: ExecutionContext[str]): briefer = await agent( system_prompt="Brief the user on weather and travel conditions.", model="anthropic/claude-sonnet-4-5", tools=[get_weather, get_forecast], response_format=Forecast, # typed output via Pydantic ) return await briefer(ctx.input)
# Working memory + long-term memory. Plus dreaming — # a background workflow that consolidates what mattered. from flux import workflow, ExecutionContext from flux.tasks.ai import agent, system_tools from flux.tasks.ai.memory import working_memory, long_term_memory, sqlite from flux.tasks.ai.dreaming import dream @workflow async def research_assistant(ctx: ExecutionContext[dict]): assistant = await agent( system_prompt="Research, store findings, recall when useful.", model="anthropic/claude-sonnet-4-5", tools=[system_tools(workspace=".")], working_memory=working_memory(), long_term_memory=long_term_memory( provider=sqlite("memory.db"), agent="researcher", scope="user:alice", ), on_complete=[dream], # consolidate memory in the background ) return await assistant(ctx.input["message"])
# Run tasks concurrently. Flux gathers, checkpoints, returns. from flux import task, workflow, ExecutionContext from flux.tasks import parallel @task async def say_hi(name): return f"Hi, {name}" @task async def say_hello(name): return f"Hello, {name}" @task async def say_hola(name): return f"Hola, {name}" @workflow async def greetings(ctx: ExecutionContext[str]): return await parallel( say_hi(ctx.input), say_hello(ctx.input), say_hola(ctx.input), )
# Chain tasks into a pipeline. Output flows in, transformed out. from flux import task, workflow, ExecutionContext from flux.tasks import pipeline @task async def multiply_by_two(x): return x * 2 @task async def add_three(x): return x + 3 @task async def square(x): return x * x @workflow async def transform(ctx: ExecutionContext[int]): return await pipeline( multiply_by_two, add_three, square, input=ctx.input, )
# Retries, fallbacks, rollbacks, timeouts — all declarative. from flux import task, workflow, ExecutionContext async def cleanup(): ... async def use_cache(): return "stale-but-safe" @task.with_options( retry_max_attempts=3, retry_delay=1, retry_backoff=2, timeout=30, fallback=use_cache, rollback=cleanup, secret_requests=['API_KEY'], cache=True, ) async def charge_customer(amount, secrets: dict = {}): return await stripe.charge(amount, key=secrets['API_KEY'])
# Auto-schedule on registration. Cron, intervals, or one-time. from flux import workflow, task, cron, ExecutionContext @task async def generate_report(date: str): return f"Report for {date}" @workflow.with_options( name="daily_report", schedule=cron("0 9 * * MON-FRI", timezone="UTC"), ) async def daily_report(ctx: ExecutionContext): report = await generate_report("today") return {"report": report} # $ flux workflow register reports.py # $ flux schedule list → daily_report_auto · 0 9 * * MON-FRI
# When linear isn't enough: define workflows as DAGs. from flux import task, workflow, ExecutionContext from flux.tasks import Graph @task async def fetch(id): ... @task async def parse(data): ... @task async def enrich(parsed): ... @task async def commit(enriched): ... @workflow async def ingest(ctx: ExecutionContext[str]): g = (Graph("ingest") .add_node("fetch", fetch) .add_node("parse", parse) .add_node("enrich", enrich) .add_node("commit", commit) .add_edge("fetch", "parse") .add_edge("parse", "enrich") .add_edge("enrich", "commit") .start_with("fetch") .end_with("commit")) return await g(ctx.input)
Flux Cloud. Same code. Zero ops.
The same engine you run locally, now managed and observable. Hosted workers, durable Postgres, replay debugging, traces. Workflows and agents, both. Drop the @workflow decorator and ship to production.
Early access opens in waves. No spam. Unsubscribe at any time.
You’re on the list.
We’ll be in touch when access opens.