Action control for AI agents — verify what an agent is about to do before it does it.
Project description
ActionRail
Verify what an AI agent is about to do — before it does it.
ActionRail is an open-source project by ToolJet.
Release status: ActionRail is currently in public beta. Interfaces may evolve before ActionRail v1, which is planned for August 2026.
Integration support: Any Python application can use the framework-neutral
ActionRuntime. LangGraph also has automatic tool discovery and wrapping; additional automatic framework adapters are planned.
An agent's tool call can be perfectly permitted and perfectly well-formed and still be wrong: a refund on an order that was already refunded, a transfer to a real account that belongs to the wrong customer, a withdrawal larger than the balance. Allowlists and schema checks pass all of these, because the shape is fine — it's the value that's wrong.
ActionRail intercepts an agent's consequential tool calls and, before execution, grounds each argument against your live system of record — then returns allow / hold-for-human / block. It's the last-mile correctness gate: not "is the agent allowed to do this?" but "is this specific value actually right, right now, against your real data?"
ActionRail runs at the execution boundary. Source lookups and credentials stay
in the customer environment, and the tool runs only after an allow decision.
Choose an integration path
The ActionRail runtime is framework-neutral. LangGraph is the first adapter that adds automatic tool discovery and wrapping.
| Your application | Integration | Start here |
|---|---|---|
| Any Python agent, worker, API, or application | Explicit ActionRuntime boundary |
Direct Python guide |
| LangGraph | Automatic discovery and tool wrapping | LangGraph quickstart |
Any Python application
Install the base SDK, create an agent and its actions in the Console, then put
ActionRuntime around the real operation:
python -m pip install actionrail actionrail-console
from actionrail import ActionRuntime
runtime = ActionRuntime(
agent_id="…",
api_key="ark_…",
)
result = runtime.execute(
"issue_refund",
{"order_id": order_id, "amount": amount},
issue_refund,
context={"customer_id": authenticated_customer_id},
)
execute() invokes issue_refund(**arguments) only after authorization. Use
the same API in custom agent loops, workers, API handlers, or frameworks without
a dedicated adapter. See the direct Python integration.
LangGraph automatic adapter
Use enforce() when you want ActionRail to discover and wrap a compiled
LangGraph agent's tools:
from actionrail import enforce, trusted_context
from actionrail.sdk.grounding import SQLiteSource
agent = enforce(
agent,
agent_id="…",
api_key="ark_…",
sources={"billing": SQLiteSource("billing.db")},
)
with trusted_context(customer_id="C-1007"):
agent.invoke(inputs)
# wire_money(account="1234567") -> blocked before the underlying tool runs
Detailed reasons remain available to local application code through the
on_decision callback. The model-facing tool result, outbound Activity, and
review records use separate value-free representations.
Run the packaged LangGraph demo
The complete deterministic demo needs no model API key:
python3 -m venv .venv
source .venv/bin/activate
python -m pip install "actionrail[agents]" actionrail-console
actionrail-console --no-open
In a second terminal:
source .venv/bin/activate
actionrail-quickstart
The demo creates a local SQLite Source, installs a grounding rule, attempts a
cross-customer refund, proves the real tool did not execute, and verifies the
redacted block in Activity. Open http://127.0.0.1:8020/activity to inspect it.
Why it's different
Policy engines and authz (OPA, Cedar, ReBAC) decide whether an action is permitted given facts you've pre-loaded. By design they don't reach out to a live system and check a value at call time — Cedar, for instance, is a side-effect-free language with no I/O. ActionRail fills exactly that gap: it queries your real data at the moment of the call and verifies the argument.
| Check | Answers | ActionRail |
|---|---|---|
| Allowlist / schema | is the call well-formed? | — |
| Policy / authz | is the agent permitted? | — |
| Grounding | is the value correct vs. live data? | ✅ |
The decision pipeline
decide() runs cheapest-check-first and returns allow / hold / block
(block outranks hold outranks allow):
| Tier | Check | Example |
|---|---|---|
| Policy | deterministic expression | amount <= 200 → over limit holds for a human |
| Grounding | value vs. system of record | account must exist, belong to the caller, and be in a refundable state → mismatch blocks |
Grounding is expressive: multiple checks ANDed across different Sources, each
comparing a returned field with an operator (= ≠ > ≥ < ≤ contains, is one of, is set, is empty) against a caller-context value, a literal, another argument
(balance ≥ amount), or the current time (delivered_at ≥ now-30d). Checks
can require a row (expect: row) or its absence (expect: absent, for
blocklists / idempotency), bind query params by name (:customer_id in SQLite,
%(customer_id)s in PostgreSQL/MySQL), and carry a retry + fail-open/closed policy.
Action Tests: rules as executable contracts
Commit expected action behavior alongside your rules and run it through the same decision pipeline used in production:
# actionrail.cases.yaml
schema_version: 1
cases:
- name: cross-customer refund is blocked
tool: issue_refund
args: {order_id: order-100, amount: 75}
context: {customer_id: customer-2}
expect: block
fixtures:
billing-production:
by_value:
order-100: {customer_id: customer-1, status: delivered}
actionrail test
# PASS cross-customer refund is blocked
# 1 passed · 0 failed
Action Tests make allow, hold, and block a reviewable CI contract.
Deterministic Source fixtures exercise the real policy evaluator and grounding
matcher without a model, Console, credentials, or network access. Production
Sources remain off unless a separate integration stage explicitly passes
--live-sources. See the Action Tests guide
or run the complete contract in examples/action_tests/.
Sources
Grounding runs against sources that stay in your environment (the data plane
never leaves your VPC). Today: SQLite, PostgreSQL, MySQL/MariaDB,
HTTP/REST, and MCP gateway adapters. MCP Sources call a configured verification tool over
Streamable HTTP, require its readOnlyHint, and prefer structured tool output. Secrets are
written as ${env:VAR} references and resolve locally, so credentials never
reach the control plane. Use a gateway identity that can invoke only read tools;
MCP annotations are hints, not an authorization boundary.
sources:
stripe-production:
adapter: mcp
endpoint: https://gateway.internal/mcp
headers:
Authorization: Bearer ${env:MCP_GATEWAY_TOKEN}
tool: stripe.get_charge
arguments:
charge_id: "{value}"
select: data.charge
timeout: 10
require_read_only: true
This keeps MCP connection details on the Source. Rules reference the Source and only describe how to evaluate the returned record; they do not duplicate the gateway endpoint, tool name, or argument mapping.
PostgreSQL Sources keep connection details on the Source and queries on rules.
Use a dedicated role with SELECT-only permissions. ActionRail also forces each
verification transaction into PostgreSQL read-only mode and applies connection
and statement timeouts. Queries use Psycopg named parameters such as
%(value)s, %(amount)s, and %(customer_id)s:
sources:
billing-production:
adapter: postgres
host: postgres.internal
port: 5432
dbname: billing
user: actionrail_reader
password: ${env:POSTGRES_PASSWORD}
sslmode: verify-full
sslrootcert: /etc/ssl/certs/postgres-ca.pem
connect_timeout: 5
statement_timeout_ms: 5000
require_read_only: true
The SDK depends on the Psycopg interface, leaving the libpq implementation to
the host application. For a self-contained local install, add
psycopg[binary]>=3.2,<4; production environments can use the system-linked
psycopg[c] build instead.
MySQL and MariaDB Sources use the same named query parameters and enforce
START TRANSACTION READ ONLY for every check. PyMySQL is included with the SDK:
sources:
billing-mysql:
adapter: mysql
host: mysql.internal
port: 3306
database: billing
user: actionrail_reader
password: ${env:MYSQL_PASSWORD}
ssl_mode: verify-identity
ssl_ca: /etc/ssl/certs/mysql-ca.pem
connect_timeout: 5
query_timeout: 5
require_read_only: true
Control plane + Console
An optional Django + DRF control plane and React Console let you register
sources, author grounding rules per argument, and watch a live Activity
feed of every allow / hold / block the runtime made. The SDK reports metadata
only — tool names, outcomes, value-free reasons, and redacted previews. Detailed
grounding results and rendered previews are exposed only on the local Decision;
model-facing tool results receive their own fully redacted representation. Preview
placeholders in control-plane reports become [redacted] unless a rule explicitly
allowlists a field with write.report_args; source-record values are never exported. Runtime reports use
a bounded, local SQLite outbox with batching, retry backoff, saturation
backpressure, and a normal-shutdown flush. Events are committed before the SDK
accepts them, stable IDs make retries idempotent, and delivery resumes after a
process restart. The outbox contains metadata but never the agent key and is
stored under ~/.actionrail/sdk/ by default; set ACTIONRAIL_SDK_STATE_DIR or
pass state_dir= to choose another location. Call close_reporting(agent) from
your application shutdown hook. A False result means rows remain durably
pending for the next process rather than being discarded.
Argument reporting is explicit and field-level; everything else remains redacted:
write:
preview: "Refund order {order_id} for {amount}"
report_args: [amount] # explicit opt-in; order_id remains [redacted]
Expose get_runtime_health(agent) from your application health endpoint to see
whether the SDK is using cached configuration, how stale its last validated
snapshot is, and whether audit rows are pending after a delivery failure.
Human-review actions always fail closed when the Console cannot be reached. The
model-facing result uses ACTIONRAIL_REVIEW_UNAVAILABLE, distinct from a normal
pending review or human rejection, and the underlying tool is never executed.
Remote enforcement accepts cached configuration for up to 24 hours by default.
After max_config_staleness is exceeded, enforcing-mode tool calls fail closed with
ACTIONRAIL_CONFIG_STALE until a refresh succeeds. Set the limit in seconds on
enforce(...); pass None only when your deployment explicitly accepts
unbounded configuration staleness.
Anonymous usage telemetry
The OSS Console and SDK send three allowlisted events to
https://hub.actionrail.ai/v1/events: console_started, sdk_initialized, and
one usage_summary snapshot per UTC day. The summary contains exact totals
for configured and active agents, observed and evaluated actions, and actions
monitored, allowed, blocked, or held. Events also contain a locally generated
anonymous installation UUID, package version, Python major/minor version, and
operating-system family. They never contain agent or tool identities, rules,
arguments, individual decisions, Source configuration, endpoints, credentials,
audit records, paths, or errors.
Disable telemetry before starting the Console or agent process:
export ACTIONRAIL_TELEMETRY_DISABLED=1
DO_NOT_TRACK=1 and CI=1 are also honored. Delivery has a one-second timeout
and cannot affect enforcement or startup. Lifecycle events are best-effort. The
Console persists at most one aggregate-only usage summary for an hourly retry;
it never queues individual activity. See Anonymous telemetry
for the complete event schema, counter definitions, and identifier lifecycle.
See Runtime availability and failure behavior for the complete startup, outage, recovery, review, and shutdown contract.
Development
Contributors using an editable repository checkout can run the packaged
quickstart implementation through python examples/langgraph_quickstart.py.
For development and the full test suite:
uv pip install -e ".[dev,control-plane]" # SDK + tests + control plane
pytest # run the suite
To run the development Console:
python control-plane/manage.py migrate && python control-plane/manage.py runserver 8020
cd frontend && npm install && npm run dev # http://localhost:5173
To build the two beta wheels for manual distribution:
python scripts/build_beta_wheels.py
# dist/actionrail-0.1.0b7-py3-none-any.whl
# dist/actionrail_console-0.1.0b7-py3-none-any.whl
# dist/SHA256SUMS
Install both wheels, then start the self-contained local Console:
python -m pip install dist/actionrail-*.whl dist/actionrail_console-*.whl
actionrail-console
Documentation site
The Docusaurus site lives in docs/:
cd docs
npm ci
npm start
Run the production build and broken-link checks with:
npm run build
See docs/README.md for deployment variables, optional Algolia
search, site structure, and contribution conventions.
Repository layout
actionrail/sdk/
runtime.py # framework-neutral evaluate / execute boundary
actions.py # explicit, privacy-safe action definitions
action_tests.py # versioned allow / hold / block regression contracts
scanner.py # zero-config action-surface discovery on a LangGraph agent
instrument.py # report the surface + observed calls to the control plane
enforce.py # the runtime gate: wrap consequential tools, decide before they run
pipeline.py # policy -> grounding -> allow / hold / block
policy.py # tier-1 deterministic policy
config.py # rule config + Source adapter build (with ${env:VAR} resolution)
report.py # metadata-only client to the control plane
safewrite.py # dry-run preview + transactional rollback (reversible sinks)
grounding/
base.py # Source protocol + GroundVerdict
match.py # shared condition matcher (operators, ctx/arg/now targets)
sqlite.py # SQLite source adapter
http.py # HTTP/REST source adapter
mcp.py # MCP gateway source adapter (read-only tools)
postgres.py # PostgreSQL source adapter (read-only transactions)
mysql.py # MySQL/MariaDB source adapter (read-only transactions)
actionrail/cli.py # `actionrail test` framework CLI
control-plane/ # Django + DRF: agents, rules, sources, decision feed
frontend/ # React console
tests/ # pytest suite (grounding, pipeline, domains, SDK, control plane)
examples/ # runnable deterministic integrations
Project status
Public beta: the framework-neutral ActionRuntime, LangGraph discovery and
enforce() adapter, policy + grounding pipeline, deterministic Action Tests,
SQLite + PostgreSQL + MySQL + HTTP + MCP Sources, control plane, and Console are
implemented and covered by the test suite.
Roadmap: provenance and taint tracking for values from untrusted memory, additional agent-framework integrations, and more vendor-specific Source adapters. ActionRail v1 is planned for August 2026.
Contributing and support
See CONTRIBUTING.md for development setup, safety-critical change requirements, and pull-request guidance. Usage questions and bug-report routing are documented in SUPPORT.md. Security reports must follow SECURITY.md.
Release changes are tracked in CHANGELOG.md. Participation in the project is governed by the Code of Conduct.
License
ActionRail is licensed under the Apache License 2.0.
Copyright 2026 ToolJet Solutions, Inc.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file actionrail-0.1.0b7.tar.gz.
File metadata
- Download URL: actionrail-0.1.0b7.tar.gz
- Upload date:
- Size: 123.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9224d805febe25e6007421722a04316dc2889445cdc052feb6d28f6da5c0b31f
|
|
| MD5 |
07fd8b3c83ff0ffba3054fdf4d202ffc
|
|
| BLAKE2b-256 |
3e10471c1d48e3c474c9f8b5ea371aef5be123db812b0d794ad4f632b3422de4
|
Provenance
The following attestation bundles were made for actionrail-0.1.0b7.tar.gz:
Publisher:
release.yml on ToolJet/ActionRail
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
actionrail-0.1.0b7.tar.gz -
Subject digest:
9224d805febe25e6007421722a04316dc2889445cdc052feb6d28f6da5c0b31f - Sigstore transparency entry: 2207562467
- Sigstore integration time:
-
Permalink:
ToolJet/ActionRail@caa1530e8b5e4c555e8f384783fea5297b461193 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/ToolJet
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@caa1530e8b5e4c555e8f384783fea5297b461193 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file actionrail-0.1.0b7-py3-none-any.whl.
File metadata
- Download URL: actionrail-0.1.0b7-py3-none-any.whl
- Upload date:
- Size: 77.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fccc59a7fa259175fd0972baa2c448eb42e93fc3de291f1c5b6864a09abcebc6
|
|
| MD5 |
2faf2d0890787cb89af252c36cc189b3
|
|
| BLAKE2b-256 |
9835f6a87e3faba61e69c05b9ec81aa76b853d3699e3b598c72eb8325565ae8a
|
Provenance
The following attestation bundles were made for actionrail-0.1.0b7-py3-none-any.whl:
Publisher:
release.yml on ToolJet/ActionRail
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
actionrail-0.1.0b7-py3-none-any.whl -
Subject digest:
fccc59a7fa259175fd0972baa2c448eb42e93fc3de291f1c5b6864a09abcebc6 - Sigstore transparency entry: 2207485191
- Sigstore integration time:
-
Permalink:
ToolJet/ActionRail@9412236727045e0b28c7af11f827e07b1e5ed8b8 -
Branch / Tag:
refs/tags/v0.1.0b7 - Owner: https://github.com/ToolJet
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@9412236727045e0b28c7af11f827e07b1e5ed8b8 -
Trigger Event:
push
-
Statement type: