AI News HubLIVE
In-site rewrite5 min read

Show HN: BoundFlow – an open-source control plane for AI agents

BoundFlow is an open-source control plane for managing unattended LLM agents and workflows. It provides cost caps, approval gates, automatic model switching, retries, and rollbacks to ensure safe and reliable agent operation.

SourceHacker News AIAuthor: alama24

Uh oh!

There was an error while loading. Please reload this page.

Notifications You must be signed in to change notification settings

Fork 0

Star 3

BranchesTags

Open more actions menu

Folders and files

NameName

Last commit message

Last commit date

Latest commit

History

53 Commits

53 Commits

.github/workflows

.github/workflows

cmd/boundflow

cmd/boundflow

docs

docs

gen/boundflow/v1

gen/boundflow/v1

internal

internal

migrations

migrations

proto/boundflow/v1

proto/boundflow/v1

sdk/python

sdk/python

.dockerignore

.dockerignore

.env.example

.env.example

.gitignore

.gitignore

CONTRIBUTING.md

CONTRIBUTING.md

Dockerfile

Dockerfile

LICENSE

LICENSE

Makefile

Makefile

QUICKSTART.md

QUICKSTART.md

README.md

README.md

buf.gen.yaml

buf.gen.yaml

buf.yaml

buf.yaml

docker-compose.dist.yml

docker-compose.dist.yml

docker-compose.yml

docker-compose.yml

go.mod

go.mod

go.sum

go.sum

mkdocs.yml

mkdocs.yml

Repository files navigation

The operational layer for the LLM agents and workflows you run unattended — cost caps, approval gates, and self-healing policy, enforced by a control plane.

Important

Public preview (pre-1.0). The engine is complete and covered by Go, mock-LLM, and live-LLM test suites, but it hasn't yet been run in production with external users. APIs — including the gRPC protobufs — may change before 1.0. We're looking for early adopters and design partners: reach out.

BoundFlow runs long-running, stateful agent workflows and enforces the guardrails you'll want before running agents unattended: per-run cost caps, automatic model switching on cost/loop policies, human approval gates before sensitive actions, tool-call limits, retries, cooldowns, and versioned rollbacks. You write agents and workflows against a clean async SDK; the control plane schedules, dispatches, and governs them.

Inference is bring-your-own — your agents call Claude with your own Anthropic key, running in your worker. The backend never sees it and never pays for tokens. Your keys, your data, and your token spend stay on your side of the wire.

In practice: a support-triage workflow that may spend up to $0.25/run, must get a human's sign-off before issuing a refund, downgrades to Haiku when costs spike, and auto-rolls-back to the last good version if it starts failing — none of that logic living in your agent code. You declare it as policy; the control plane enforces it and keeps a durable, queryable audit log of every approval and policy decision.

BoundFlow is not a prompt framework, an inference provider, or an agent-builder — it's the operational layer around the agents you build.

Backend — open source (Apache-2.0), self-hostable as a container.

Python SDK — open source (MIT), pip install boundflow.

Docs — concepts, governance, deployment, and API reference in docs/.

BoundFlow Cloud — prefer not to self-host? Managed hosting (early access) — see below.

Why BoundFlow

Agents that take real actions need a control plane that takes real action when they go wrong. Most tools watch your agents; BoundFlow intervenes — at both levels. On the agent: cap its spend, swap its model mid-run. On the workflow: gate a risky step for human sign-off, cool it down, roll it back to a known-good version, or pause it outright. It's workflow-aware, not just agent-aware — because it runs the whole workflow, not just the model call: scheduling each run, carrying state across steps, recovering from failures, and driving it through its lifecycle, with the agent as just one operation inside a durable, multi-step process it owns end to end.

The moment agents run unattended you need answers to: What if it loops? What if it spends $50? What if it's about to do something irreversible? Which model should it use, and when should that change? BoundFlow makes those policies instead of code:

Concern BoundFlow gives you

Runaway cost A hard max_cost_usd cap that halts a run the moment its cost crosses budget

Irreversible actions Approval gates — the workflow parks for a human decision before it acts

Loops & output blowups Runtime limits: max_llm_calls, max_tokens_per_call, per-tool call caps

Wrong model for the job Agent lifecycle policy — react to signals over the agent's entire life (e.g. downgrade a costly model to a cheaper one past a certain budget)

Degrading or failing workflows Self-healing lifecycle policy — cool down, pause, or auto-roll-back to a known-good version

Flying blind OpenTelemetry-native run traces shipped to your stack (Jaeger, Tempo, Langfuse, …), plus a durable, queryable audit log of every approval and policy decision

Your keys & token spend Bring-your-own inference — agents call Claude with your key; the backend never sees it or pays for tokens (cache-aware, per-tenant cost)

Policies are evaluated server-side (lifecycle) and enforced SDK-side (runtime), with per-invocation metrics — cost, tokens, LLM calls, per-tool counts/failures — collected on every run.

Architecture

The BoundFlow backend is the control plane — self-host it, or run it on BoundFlow Cloud. Either way, your worker connects to it over gRPC and runs the actual agents, with your Anthropic key, in your environment; the backend schedules, dispatches, governs, and audits, and never sees your key or your inference traffic.

┌─────────────────────┐ gRPC ┌────────────────────────┐ │ Your client / SDK │ ───────────────▶ │ │ └─────────────────────┘ invoke·approve │ BoundFlow backend │ ·query │ (control plane) │ ┌─────────────────────┐ gRPC stream │ │ │ Your worker │ ◀──────────────▶ │ schedules·dispatches │ │ runs agents+tools │ launch/result │ ·governs·audits │ │ with your API key │ └────────────────────────┘ └─────────────────────┘

Under the hood the backend runs as three process modes (server, scheduler, worker) off one binary sharing Postgres — see docs/concepts.md for the full breakdown and the lifecycle states.

SDK at a glance

from boundflow import AgentDefinition, BoundFlowWorker, Complete, ControlPlaneClient, WorkflowConfig from boundflow.anthropic_client import AnthropicLlmClient

worker = BoundFlowWorker(llm=AnthropicLlmClient(...)) # endpoints + key from env

@worker.workflow("triage", version=1) async def triage(ctx): ctx.add_context("ticket", "...") await ctx.run_agent(AgentDefinition( name="analyst", model="claude-haiku-4-5", system_prompt="Diagnose the issue.", output_schema={"summary": {"type": "string"}}, )) return Complete()

Bring your own provider via LangChain. Wrap any tool-calling LangChain chat model in LangChainLlmClient and the governance is identical — OpenAI, Google, Bedrock, and the rest of LangChain's ecosystem run under the same cost caps, model policies, and approval gates:

from langchain_anthropic import ChatAnthropic # or ChatOpenAI, ChatVertexAI, ... from boundflow.langchain_client import LangChainLlmClient

worker = BoundFlowWorker(llm=LangChainLlmClient(ChatAnthropic(model="claude-haiku-4-5")))

Install with pip install "boundflow[langchain]"; see boundflow.examples.langchain_adapter for a runnable end-to-end example.

Orchestrate with LangGraph, governed by BoundFlow. Build a LangGraph agent graph inside a workflow with its nodes calling ctx.run_agent, so LangGraph owns the routing while BoundFlow governs every agent step and the workflow as a whole. See Integrations and the runnable boundflow.examples.langgraph_workflow.

Workflows are multi-step and stateful: an operation can park for a human decision or chain into a follow-on operation, and the workflow resumes where it left off — nothing irreversible runs until the branch it's gated behind does.

from boundflow import AwaitApproval, Next, Complete

@worker.workflow("refund", version=1) async def refund(ctx): await ctx.run_agent(analyst) # step 1: reason about the request return AwaitApproval( # park — nothing irreversible yet on_approve=Next("issue_refund", ctx.context), on_reject=Complete(), justification="Approve the $5,000 refund?", )

@worker.operation("refund", "issue_refund") # step 2: runs only after a human approves async def issue_refund(ctx): ... # the sensitive action, now sanctioned return Complete()

Governance is applied from the control plane — three layers, from a per-run cap to self-healing version rollback:

from boundflow import ( RuntimePolicy, AgentRule, AgentMetric, Op, SetModel, WorkflowRule, WorkflowMetric, SetVersion, )

1. Runtime — a hard cap enforced *during* every run:

await cp.set_agent_runtime_policy(wf.id, "analyst", RuntimePolicy(max_cost_usd=0.25))

2. Agent lifecycle — after runs, downgrade the model if cost trends high:

await cp.set_agent_lifecycle_policy(wf.id, "analyst", [ AgentRule(metric=AgentMetric.COST_USD, op=Op.GT, threshold=0.20, window=5, action=SetModel(value="claude-haiku-4-5")), ])

3. Workflow lifecycle — after repeated failures, roll the whole workflow back

to a known-good version automatically:

await cp.set_workflow_lifecycle_policy(wf.id, [ WorkflowRule(metric=WorkflowMetric.NUM_FAILURES, threshold=3, action=SetVersion(target=1)), ])

Workflow rules can also Pause a workflow or put it on Cooldown instead of rolling back. See sdk/python/boundflow/examples/ for runnable examples.

Quick start

Get a governed agent running in a few minutes. Full walkthrough: QUICKSTART.md.

1. Set a database password (any strong secret)

echo "BOUNDFLOW_DB_PASSWORD=$(openssl rand -hex 16)" > .env

2. Start the backend (Postgres + server + scheduler + worker)

docker compose -f docker-compose.dist.yml up -d

3. Provision an API key

docker compose -f docker-compose.dist.yml run --rm server -mode=provision -name=me export BOUNDFLOW_API_KEY=

4. Install the SDK and bring your Anthropic key

pip install boundflow export ANTHROPIC_API_KEY=

5. Run a real agent under governance

python -m boundflow.examples.hello

Then explore the bundled examples:

python -m boundflow.examples.approval_gate # human-in-the-loop sign-off

Manage and observe it from the boundflow CLI (installed with the SDK):

boundflow workflow list # your workflows and their state boundflow workflow runs # runs and their outcomes · --json for scripting

Observability

Observability is first-class and OpenTelemetry-native — no proprietary format, no lock-in, so it plugs straight into the telemetry stack you already run. Two layers: run traces (execution telemetry you export to your own backend) and a governance audit log (decisions, kept server-side and queryable).

Run traces. Every operation emits an OperationTrace — the operation → agent → llm/tool tree with token usage and full prompt/response content — to a pluggable sink you own. Built-ins: LoggingTraceSink, JsonlFileTraceSink, and OTelTraceSink, which maps onto OpenTelemetry GenAI semantic conventions and ships spans over OTLP to any backend (Jaeger, Tempo, Langfuse, Phoenix, …); all operations of one run share a trace_id.

from boundflow import BoundFlowWorker from boundflow.trace import OTelTraceSink

worker = BoundFlowWorker(llm=..., trace_sink=OTelTraceSink(tracer))

See sdk/python/examples/otel/ for a runnable OTLP → Jaeger setup.

Approval audit. Approval decisions are governance, not telemetry, so the decision / actor / timing live in a durable server-side audit log — the trace carries only the approval_id (on the await_approval span) as the correlation key. Look the record up by that id:

records = await cp.get_approval_audit(approval_id="…")

-> decision (approved | rejected | timed_out), actor, opened_at, decided_at

Inventory. cp.list_workflows() returns every workflow with its current lifecycle / workflow state for dashboards.

Configuration

Backend and SDK are configured through BOUNDFLOW_* environment variables (plus ANTHROPIC_API_KEY for real agents). See docs/deployment.md for the full reference and the TLS-termination setup.

The

[truncated for AI cost control]