Scheduled Actions¶
Scheduled actions let you delay action execution by a configurable duration. Instead of dispatching immediately, Acteon stores the action and executes it after the specified delay. This is useful for:
- Delayed notifications -- send a reminder email 24 hours after signup
- Off-peak retries -- reschedule failed actions to run during low-traffic windows
- Escalation workflows -- if an alert is not acknowledged within 30 minutes, escalate to the on-call manager
How It Works¶
sequenceDiagram
participant C as Client
participant G as Gateway
participant S as State Store
participant BG as Background Processor
participant P as Provider
C->>G: Dispatch action (send_reminder)
G->>G: Evaluate rules
Note over G: Rule matches → schedule with 3600s delay
G->>S: Store action (due at now + 3600s)
G-->>C: Scheduled (action_id, scheduled_for)
Note over BG,S: 3600 seconds later...
BG->>S: Poll for due actions
S-->>BG: Action is due
BG->>P: Execute action
P-->>BG: Success
BG->>S: Mark completed - The client dispatches an action through the normal
POST /v1/dispatchendpoint - The gateway evaluates rules -- if a
schedulerule matches, the action is stored for later - The gateway returns an
ActionOutcome::Scheduledresponse with the action ID and scheduled time - A background processor polls the state store for due actions at a configurable interval
- When an action's scheduled time arrives, the background processor dispatches it to the provider
Rule Configuration¶
YAML¶
rules:
- name: delay-reminders
priority: 10
description: "Send reminder emails after a 1-hour delay"
condition:
field: action.action_type
eq: "send_reminder"
action:
type: schedule
delay_seconds: 3600
- name: offpeak-batch-reports
priority: 15
description: "Delay batch reports by 6 hours to run off-peak"
condition:
all:
- field: action.action_type
eq: "generate_report"
- field: action.metadata.priority
eq: "low"
action:
type: schedule
delay_seconds: 21600
CEL¶
// Delay reminders by 1 hour
action.action_type == "send_reminder" ? schedule(3600) : allow()
// Delay low-priority reports by 6 hours
action.action_type == "generate_report" && action.metadata.priority == "low" ? schedule(21600) : allow()
Parameters¶
| Parameter | Type | Required | Description |
|---|---|---|---|
delay_seconds | u64 | Yes | Delay before execution, in seconds (1--604800) |
The maximum delay is 604800 seconds (7 days).
Response¶
When an action matches a schedule rule, the dispatch endpoint returns:
{
"outcome": "scheduled",
"action_id": "019462a1-7b3e-7f00-a123-456789abcdef",
"scheduled_for": "2026-01-15T12:00:00Z"
}
| Field | Type | Description |
|---|---|---|
outcome | string | Always "scheduled" |
action_id | string | Unique identifier for the scheduled action |
scheduled_for | string | RFC 3339 timestamp when the action will be dispatched |
Server Configuration¶
Enable the background processor in acteon.toml:
| Key | Type | Default | Description |
|---|---|---|---|
enable_scheduled_actions | bool | false | Enable the scheduled actions background processor |
scheduled_check_interval | u64 | 5 | How often to poll for due actions, in seconds |
Warning
If enable_scheduled_actions is false, schedule rules will still match and store actions, but nothing will dispatch them. Always enable the background processor in production.
Client SDK Usage¶
Scheduled actions work through the existing dispatch() API. No new client methods are needed -- just handle the new Scheduled outcome type in the response.
Rust¶
use acteon_client::ActeonClient;
let client = ActeonClient::new("http://localhost:8080");
let outcome = client.dispatch(&action).await?;
match &outcome {
ActionOutcome::Scheduled { action_id, scheduled_for } => {
println!("Action {} scheduled for {}", action_id, scheduled_for);
}
ActionOutcome::Executed(resp) => {
println!("Executed immediately: {:?}", resp);
}
_ => {}
}
Python¶
from acteon_client import ActeonClient
client = ActeonClient("http://localhost:8080")
outcome = client.dispatch(action)
if outcome.is_scheduled():
print(f"Action {outcome.action_id} scheduled for {outcome.scheduled_for}")
elif outcome.is_executed():
print(f"Executed immediately: {outcome.response}")
Node.js / TypeScript¶
const outcome = await client.dispatch(action);
if (outcome.type === "scheduled") {
console.log(`Action ${outcome.actionId} scheduled for ${outcome.scheduledFor}`);
} else if (outcome.type === "executed") {
console.log(`Executed immediately:`, outcome.response);
}
Go¶
outcome, err := client.Dispatch(ctx, action)
if err != nil {
log.Fatal(err)
}
if outcome.IsScheduled() {
fmt.Printf("Action %s scheduled for %s\n", outcome.ActionID, outcome.ScheduledFor)
} else if outcome.IsExecuted() {
fmt.Println("Executed immediately:", outcome.Response)
}
Java¶
ActionOutcome outcome = client.dispatch(action);
if (outcome.isScheduled()) {
System.out.printf("Action %s scheduled for %s%n",
outcome.getActionId(), outcome.getScheduledFor());
} else if (outcome.isExecuted()) {
System.out.println("Executed immediately: " + outcome.getResponse());
}
Reliability¶
Scheduled actions use at-least-once delivery semantics:
- Actions are persisted to the state store before the client receives a response
- The background processor marks actions as "in-flight" before dispatching
- If the processor crashes mid-dispatch, the action will be retried on the next poll
- Successfully dispatched actions are marked as completed and will not be re-dispatched
A 24-hour grace period applies: actions that remain in the "in-flight" state for more than 24 hours are considered stale and will be retried. This handles cases where the processor crashes after picking up an action but before marking it complete.
Limitations¶
- Maximum delay: 7 days (604800 seconds). Longer delays are rejected at rule evaluation time.
- No cancellation API: Once an action is scheduled, it cannot be cancelled. A cancellation endpoint is planned for a future release.
- Polling latency: The background processor polls at the configured interval (
scheduled_check_interval), so actions may be dispatched up to N seconds after their scheduled time, where N is the check interval. - Single processor: The background processor runs as a single instance. High-availability deployments should use leader election or a distributed lock to prevent duplicate dispatches.
See also
For actions that need to fire on a recurring cron schedule (e.g., daily digests, weekly reports), see Recurring Actions.