Skip to main content

Phase 2: Add Signals

You are here if: you have completed Phase 1 (your agents are observed) and you want centralized control over when and how your agents are triggered.

What you will have after this phase: webhook-driven agent execution with retry logic, audit trails, and a single control point for all your agents.


What Are Signals?​

A signal is a typed HTTP webhook that triggers an agent run through the Waxell control plane. Instead of calling your agent function directly, you POST a signal payload to Waxell, and the control plane routes it to the right agent with full governance applied.

Your App ──POST──> Waxell Control Plane ──triggers──> Your Agent
│
├── Policy check
├── Rate limiting
├── Audit logging
└── Retry on failure

Signals decouple your application code from your agent execution. Your app does not need to know where the agent runs, how it is configured, or what policies apply -- it just emits a signal.


How Signals Work​

1. Define a Signal in Your Agent​

If you are already using Waxell Observe, your agents are tracked but triggered directly. Signals start agents that run on Waxell, so the agent is declared in a waxell.yaml manifest (or with @agent in Python) and pushed with wax push. Declare the signal it listens for alongside it:

agents:
- name: support_classifier
signals:
- name: support_ticket
source_type: api
schema:
customer_id: string
message: string

See the waxell.yaml reference for every signal field.

2. Emit Signals from Your Application​

Replace direct agent calls with signal emissions using the Waxell SDK client:

Before (direct call):

# Your application calls the agent directly
result = await handle_support_ticket(customer_id="C-1234", message="I can't log in")

After (signal-driven):

from waxell import WaxellClient

client = WaxellClient()

result = await client.emit_signal(
signal_name="support_ticket",
payload={
"customer_id": "C-1234",
"message": "I can't log in",
},
)

if result.success:
print(f"Signal emitted: {result.signal_id}")
else:
print(f"Error: {result.error}")

3. Configure the Client​

The WaxellClient reads configuration from multiple sources (highest priority first):

  1. Explicit constructor arguments
  2. Global configuration via WaxellClient.configure()
  3. CLI config file (~/.waxell/config)
  4. Environment variables (WAX_API_URL, WAX_API_KEY)
# Option A: Environment variables (recommended for production)
# export WAX_API_URL="https://acme.waxell.dev"
# export WAX_API_KEY="wax_sk_..."
client = WaxellClient()

# Option B: Global configuration at app startup
WaxellClient.configure(
api_url="https://acme.waxell.dev",
api_key="wax_sk_...",
)
client = WaxellClient()

# Option C: Per-instance (overrides everything)
client = WaxellClient(
api_url="https://acme.waxell.dev",
api_key="wax_sk_...",
)

For synchronous contexts (e.g., Django views without async), use the sync variant:

result = client.emit_signal_sync(
signal_name="support_ticket",
payload={"customer_id": "C-1234", "message": "I can't log in"},
)

What You Gain​

Centralized Orchestration​

All agent triggering flows through the Waxell control plane. You can see every signal that was emitted, which agent it triggered, and what happened.

Retry Logic​

If the agent fails, the control plane can retry the execution based on your configured retry policy. Your application does not need to implement retry logic.

Audit Trail​

Every signal emission is logged with:

  • Who emitted it (API key / tenant)
  • When it was emitted
  • The full payload
  • Which agent run it triggered
  • The execution result

Scheduling​

Agents can run on a schedule. Declare a schedule: block in the agent's manifest — a five-field cron in UTC, optionally naming the signal and payload to fire — or manage schedules from the CLI with wax agents schedule. See Operating Agents.

agents:
- name: support_classifier
schedule:
- cron: "0 9 * * *"
signal: support_ticket
payload: {customer_id: "digest", message: "daily sweep"}

Integration Pattern: Existing Observed Agents​

An agent wrapped with @waxell_agent or WaxellContext (from Phase 1) runs in your own process, and your application keeps calling it directly. Its telemetry and policy checks continue to flow to Waxell, so you can adopt signals one agent at a time:

  1. Declare the agent in waxell.yaml with its signals: block (as above) and push it with wax push.
  2. Replace the direct call in your application with client.emit_signal(...).
  3. Keep the observed version running until the signal-driven run produces the same results in Observe → Runs, then retire it.

The signal payload fields (customer_id, message) become the run's input.


What You Do Not Get (Yet)​

Phase 2 adds centralized triggering but your agents still run as standard Python functions. The following require further migration:

  • Durable workflows: Checkpoint/resume requires native Waxell workflows (Phase 4)
  • Approval workflows: Pause/resume with human-in-the-loop requires native Waxell (Phase 4)
  • Domain abstraction: Governed external system calls require native Waxell domains (Phase 4)
  • LLM routing: Automatic model selection and fallbacks require native Waxell (Phase 4)

Next Steps​