Skip to content

Cycles Action Authority Demo

An agent can use CRM, notes, and email — but Cycles decides which actions are allowed before execution.

Same agent. Same tools. The next unapproved action never executes.

The scenario

A support automation agent handles customer case #4782 (invoice discrepancy). The agent's workflow has four steps:

  1. Read case — load case details from the CRM
  2. Append internal note — log an internal investigation note
  3. Update CRM status — change the case from Open to Investigating
  4. Send customer email — reply to the customer with an update

Steps 1–3 are internal, low-risk operations. Step 4 is a consequential, customer-facing action. In production, you want a human to approve outbound emails before they go out. Cycles enforces this by setting a zero-dollar budget for unapproved toolsets.

Without Cycles, all four steps execute — including the email. With Cycles, the server returns 409 BUDGET_EXCEEDED before the email send can proceed, and the agent reports: "Email blocked — not approved for autonomous execution. Escalated to human review."

No real CRM, email service, or ticketing system is used. All tools are mocked locally. The action authority is real.

Run it

Prerequisites: Docker Compose v2+, Python 3.10+, curl

git clone https://github.com/runcycles/cycles-agent-action-authority-demo
cd cycles-agent-action-authority-demo
python3 -m venv .venv && source .venv/bin/activate
pip install -r agent/requirements.txt
./demo.sh

That's it. The script starts the Cycles stack (Redis + server + admin), provisions a tenant with toolset-scoped budgets, then runs both modes back to back.

Run a single mode:

./demo.sh unguarded    # without Cycles (all actions execute)
./demo.sh guarded      # with Cycles (email blocked)
./demo.sh both         # both back to back (default)

Re-runs just work — the script resets the stack automatically to ensure a clean state.

Stop the stack when done:

./teardown.sh

Windows (WSL)

The demo runs on Windows 11 via WSL. Install Docker Desktop for Windows with the WSL 2 backend enabled (the default), then inside your WSL terminal:

sudo apt update && sudo apt install -y python3-full curl
git clone https://github.com/runcycles/cycles-agent-action-authority-demo
cd cycles-agent-action-authority-demo
python3 -m venv .venv && source .venv/bin/activate
pip install -r agent/requirements.txt
./demo.sh

Docker Desktop shares the daemon between Windows and WSL automatically — no extra configuration needed.

Note: Ubuntu 23.04+ requires python3-full (not just python3) so that venvs get their own pip. Without it, even pip inside a venv hits the PEP 668 "externally-managed-environment" error.

First run notes

The first run pulls three Docker images (~200MB total). You'll see Docker's pull progress. Subsequent runs start in seconds.

Troubleshooting

JedisConnectionException: Failed to create socket / UnknownHostException: redis You started the stack with docker compose up directly (instead of ./demo.sh) on top of a previous run, and the cycles-server container ended up on a stale network where the redis service no longer exists. Reset and restart:

docker compose down -v && docker compose up -d

./demo.sh does this automatically before every run, so the script path doesn't hit this.

What you'll see

Cycles Action Authority Demo

The GIF runs both modes back to back in ~30 seconds: the unguarded segment sends the customer email unchecked, then "MODE 2: WITH CYCLES" runs the same workflow and the email is blocked at the send-email toolset (zero-dollar budget → 409 BUDGET_EXCEEDED). The end card contrasts 4 / 4 actions executed against 3 / 4 executed · 1 blocked and shows the per-toolset authority decisions.

For homepage embedding, also available as demo.mp4 (H.264) and demo.webm (VP9). All assets are recorded at 2000×1200 (2× retina density) so text stays crisp on HiDPI displays even when the browser scales them down to ~1000×600 for layout. Both videos autoplay inline:

<video autoplay loop muted playsinline poster="demo-action-authority-poster.png">
  <source src="demo.webm" type="video/webm">
  <source src="demo.mp4" type="video/mp4">
  <img src="demo.gif" alt="Cycles Action Authority Demo">
</video>

demo-action-authority-poster.png is the last-frame summary card — used as the poster attribute so autoplay-blocked browsers, slow- network first paint, and social/SEO link previews all show the SENT vs DENY contrast even before the video plays.

Without Cycles

All four actions execute with green checkmarks — including the customer email. The final panel reads:

"All actions executed — including the customer email. In production: no authorization gate existed. The email went out unchecked."

With Cycles (action authority)

The first three actions execute with green checkmarks. The fourth (send email) shows a red X. The final panel reads:

"Cycles blocked the customer email before it was sent. Internal actions proceeded. Customer-facing action not approved for autonomous execution."

Expected output

⚡ Cycles — Action Authority Demo

Resetting stack (clean state)...
Starting Cycles stack...

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  MODE 1: Without Cycles
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

╭───────────────── Support Case #4782 ──────────────────╮
│ Customer:  Acme Corp (jane@acme.com)                  │
│ Subject:   Invoice shows $847, contract $720          │
│ Agent:     support-bot                                │
│ Mode:      UNGUARDED                                  │
╰───────────────────────────────────────────────────────╯

╭───────────────── Action Log ──────────────────────────╮
│                                                       │
│  ✓ read_case                                          │
│    Loaded case #4782 — Acme Corp                      │
│                                                       │
│  ✓ append_internal_note  [internal-notes]             │
│    Billing discrepancy: $847 vs $720. Investigating.  │
│                                                       │
│  ✓ update_crm_status     [crm-updates]                │
│    Status: Open → Investigating                       │
│                                                       │
│  ✓ send_customer_email   [send-email]                 │
│    Email sent to jane@acme.com                        │
│                                                       │
╰───────────────────────────────────────────────────────╯

╭───────────────── Result — UNGUARDED ──────────────────╮
│ All actions executed — including the customer email.  │
│ 4 actions approved · 0 actions blocked                │
╰───────────────────────────────────────────────────────╯

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  MODE 2: With Cycles (action authority)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

╭───────────────── Support Case #4782 ──────────────────╮
│ Customer:  Acme Corp (jane@acme.com)                  │
│ Subject:   Invoice shows $847, contract $720          │
│ Agent:     support-bot                                │
│ Mode:      GUARDED                                    │
╰───────────────────────────────────────────────────────╯

╭───────────────── Action Log ──────────────────────────╮
│                                                       │
│  ✓ read_case                                          │
│    Loaded case #4782 — Acme Corp                      │
│                                                       │
│  ✓ append_internal_note  [internal-notes]             │
│    POST /v1/reservations → 200 ALLOW                  │
│    Billing discrepancy: $847 vs $720. Investigating.  │
│                                                       │
│  ✓ update_crm_status     [crm-updates]                │
│    POST /v1/reservations → 200 ALLOW                  │
│    Status: Open → Investigating                       │
│                                                       │
│  ✗ send_customer_email   [send-email]                 │
│    POST /v1/reservations → 409 BUDGET_EXCEEDED        │
│    Email blocked — escalated to human review.         │
│                                                       │
╰───────────────────────────────────────────────────────╯

╭───────────────── Result — GUARDED ────────────────────╮
│ Cycles blocked the customer email before it was sent. │
│ 3 actions approved · 1 action blocked                 │
╰───────────────────────────────────────────────────────╯

Demo complete.
  Swagger UI:   http://localhost:7878/swagger-ui.html
  Admin UI:     http://localhost:7979/swagger-ui.html
  Re-run:       ./demo.sh
  Stop stack:   ./teardown.sh

The code change

The diff between agent/unguarded.py and agent/guarded.py is:

# --- Import the SDK ---
from runcycles import BudgetExceededError, CyclesClient, CyclesConfig, cycles, set_default_client

# --- Initialize the client ---
config = CyclesConfig(
    base_url=os.environ["CYCLES_BASE_URL"],
    api_key=os.environ["CYCLES_API_KEY"],
    tenant=os.environ["CYCLES_TENANT"],
    agent="support-bot",
    workspace="default",
    app="default",
    workflow="default",
)
set_default_client(CyclesClient(config))

# --- Add three decorators with toolset scoping ---
@cycles(estimate=COST_PER_ACTION_MICROCENTS, action_kind="tool.notes", action_name="append-note", toolset="internal-notes")
def append_internal_note(...): ...

@cycles(estimate=COST_PER_ACTION_MICROCENTS, action_kind="tool.crm", action_name="update-status", toolset="crm-updates")
def update_crm_status(...): ...

@cycles(estimate=COST_PER_ACTION_MICROCENTS, action_kind="tool.email", action_name="send-reply", toolset="send-email")
def send_customer_email(...): ...

# --- Catch the budget exception ---
except BudgetExceededError:
    # email blocked — not approved for autonomous execution

Three decorators. One except. The next unapproved action never executes.

How it works

The key mechanism is toolset-scoped budgets. The provisioning script creates budgets for:

Toolset Budget Result
internal-notes $1.00 ✓ Agent can append notes
crm-updates $1.00 ✓ Agent can update CRM
send-email $0 ✗ Zero budget → 409

When the @cycles decorator tries to reserve budget in a scope with a zero-dollar budget, the Cycles server returns 409 BUDGET_EXCEEDED. The decorator raises BudgetExceededError, and the action never executes.

This means you control agent capabilities at the budget provisioning level — no code changes needed to approve or revoke a tool. Increase the send-email budget and the agent can send emails. Set it to zero and the agent can't.

Why this matters

Static tool allowlists can't adapt at runtime. API keys grant blanket access. Role-based permissions don't distinguish between "the agent CAN send email" and "this specific run SHOULD send email." Cycles enforces a runtime decision before each consequential action — without changing agent code.

Next steps

After running the demo, explore how to add Cycles to your own application:

Links