Integrate any Python application
ActionRuntime is ActionRail's framework-neutral execution boundary. Give it
an action name, arguments, and the operation to call. It runs the same policy,
live grounding, review, availability, and audit pipeline as the LangGraph
adapter, then invokes the operation only after an allow decision.
Use this API for custom agent loops, background workers, API handlers, other orchestration frameworks, or any consequential Python operation.
LangGraph is ActionRail's current automatic adapter: it discovers the graph's
tools and wraps them for you. With ActionRuntime, you define the action
surface in the Console or in code and place the guard at the operation boundary.
1. Install the lightweight runtime
python -m pip install actionrail actionrail-console
The base SDK does not install LangGraph or LangChain. Install
actionrail[agents] only when using the LangGraph adapter or packaged
LangGraph quickstart.
2. Define the action surface
Start the Console, create an agent, and open its detail page:
actionrail-console --no-open
Choose Add action and enter the operation name and argument names—for
example, issue_refund with order_id, amount. Then configure its policy and
grounding rules in the Console. This manual surface remains in place when the
direct runtime connects; ActionRail does not require framework introspection.
After creating the action, expand Integration code on its detail page. The
recommended Guard and execute snippet places ActionRuntime.execute() at
the operation boundary. A secondary Decision only snippet uses
evaluate() when the application must enforce the returned verdict itself.
LangGraph users do not need either snippet because enforce() discovers and
wraps their tools automatically.
The generated code is a starter for that specific action and expects the agent
key in ACTIONRAIL_AGENT_KEY; it does not embed the credential. Long-running
services should create and reuse one runtime during application startup and
close it during shutdown, as described in Runtime lifecycle.
Keep the one-time agent key shown at registration. The agent ID identifies the configuration; the agent key authenticates this runtime process.
3. Guard the real operation
import os
from actionrail import ActionBlocked, ActionHeld, ActionRuntime
def issue_refund(order_id: str, amount: int):
return billing.refund(order_id=order_id, amount=amount)
with ActionRuntime(
agent_id=os.environ["ACTIONRAIL_AGENT_ID"],
api_key=os.environ["ACTIONRAIL_AGENT_KEY"],
endpoint=os.environ.get(
"ACTIONRAIL_ENDPOINT",
"http://127.0.0.1:8020",
),
) as runtime:
try:
result = runtime.execute(
"issue_refund",
{"order_id": proposed_order, "amount": proposed_amount},
issue_refund,
context={"customer_id": authenticated_customer_id},
)
except ActionBlocked:
return "The refund did not pass live verification."
except ActionHeld:
return "The refund was not approved for execution."
execute() calls issue_refund(**arguments) only after authorization. Prefer
it over a separate evaluate() followed by an operation: keeping the decision
and effect in one call makes accidental bypasses easier to prevent.
The context mapping must come from authenticated host-application state. Do
not copy model-generated values into trusted context.
The direct API can protect only the code paths that call it. Route every
invocation of a consequential operation through execute() or aexecute(),
and keep the unguarded implementation private to that boundary.
Define actions in code instead
For code-first action inventories, register privacy-safe metadata when the runtime starts:
from actionrail import ActionDefinition, ActionRuntime
runtime = ActionRuntime(
agent_id=os.environ["ACTIONRAIL_AGENT_ID"],
api_key=os.environ["ACTIONRAIL_AGENT_KEY"],
actions=[
ActionDefinition(
name="issue_refund",
description="Refund a delivered order",
arguments=("order_id", "amount"),
critical_arguments=("order_id", "amount"),
)
],
)
Definitions contain names and classifications only; they never contain runtime
argument values. A code-defined action takes precedence over a manual action
with the same name; other manual actions remain registered. If you prefer to
manage the inventory entirely in the Console, omit actions=.
Inspect a decision without executing
decision = runtime.evaluate(
"issue_refund",
{"order_id": proposed_order, "amount": proposed_amount},
context={"customer_id": authenticated_customer_id},
)
if decision.allowed:
...
elif decision.held:
...
elif decision.blocked:
...
Use evaluate() for previews and diagnostics. For production effects,
execute() is the safer integration surface.
ActionBlocked and ActionHeld both inherit from ActionNotAllowed and carry
the local decision. Detailed reasons may contain sensitive runtime data; do
not return them to a model or export them without review.
Async operations
result = await runtime.aexecute(
"send_payment",
{"account_id": account_id, "amount": amount},
payments.send,
context={"customer_id": authenticated_customer_id},
)
aexecute() supports sync and async callables without blocking the event loop
during verification or review waits. aevaluate() is the decision-only async
equivalent.
Local configuration without the Console
Pass a rule mapping, a GroundConfig, or an actionrail.yaml path as
config= when the application must run without a control plane:
runtime = ActionRuntime(
{
"tools": {
"issue_refund": {
"kind": "consequential",
"args": {"amount": {"policy": "amount <= 200"}},
}
}
}
)
Local mode has no remote Activity or review queue. The repository's runnable
framework_neutral_runtime.py
example demonstrates an allowed operation and proves that a held operation is
never invoked.
Lifecycle and health
Managed runtimes refresh configuration and deliver audit reports in the
background. Use the context manager, or call runtime.close(timeout=7) during
shutdown. runtime.flush() waits for current audit reports without closing,
and runtime.health exposes configuration and delivery status for application
health checks.
See Runtime lifecycle and Runtime availability for production failure behavior.