> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/nearai/ironclaw/llms.txt
> Use this file to discover all available pages before exploring further.

# Routines and Automation

> Persistent, scheduled tasks with triggers, guardrails, and execution modes

## Overview

Routines are named, persistent, user-owned tasks that fire automatically based on triggers (cron schedules, events, webhooks, or manual invocation). Each routine has an action (lightweight LLM call or full job) and guardrails to prevent runaway execution.

## Architecture

```text theme={null}
┌──────────┐     ┌─────────┐     ┌──────────────────┐
│  Trigger  │────▶│ Engine  │────▶│  Execution Mode  │
│ cron/event│     │guardrail│     │lightweight│full_job│
│ webhook   │     │ check   │     └──────────────────┘
│ manual    │     └─────────┘              │
└──────────┘                               ▼
                                    ┌──────────────┐
                                    │  Notify user │
                                    │  if needed   │
                                    └──────────────┘
```

## Core Concepts

### Routine Structure

Every routine has:

* **ID**: Unique UUID
* **Name**: Human-readable identifier
* **Description**: What the routine does
* **Trigger**: When to fire (cron/event/webhook/manual)
* **Action**: What to execute (lightweight/full\_job)
* **Guardrails**: Safety constraints (cooldown, concurrency, dedup)
* **Notify Config**: Where and when to send notifications

### Triggers

#### Cron Trigger

Fires on a schedule:

```yaml theme={null}
trigger:
  type: cron
  schedule: "0 9 * * MON-FRI"  # 9 AM weekdays
```

Supported formats:

* Standard cron: `"0 9 * * MON-FRI"`
* Human-readable: `"every 2h"`, `"daily at 9am"`

#### Event Trigger

Fires when a channel message matches a pattern:

```yaml theme={null}
trigger:
  type: event
  channel: telegram  # Optional filter
  pattern: "deploy\\s+\\w+"  # Regex pattern
```

#### Webhook Trigger

Fires on incoming HTTP POST:

```yaml theme={null}
trigger:
  type: webhook
  path: custom-webhook  # Optional suffix (defaults to routine ID)
  secret: my-secret     # Optional HMAC validation
```

Webhook URL: `https://your-domain.com/hooks/routine/{id}`

#### Manual Trigger

Only fires via CLI or tool call:

```yaml theme={null}
trigger:
  type: manual
```

### Actions

#### Lightweight Action

Single LLM call, no tools. Fast and cheap:

```yaml theme={null}
action:
  type: lightweight
  prompt: |
    Check for open PRs labeled 'urgent' and summarize them.
  context_paths:
    - context/priorities.md
  max_tokens: 4096
```

Execution:

1. Load context files from workspace
2. Send single LLM request with prompt + context
3. Check response for `ROUTINE_OK` (nothing to report) or content (attention needed)
4. Send notification based on status

#### Full Job Action

Multi-turn agent with full tool access:

```yaml theme={null}
action:
  type: full_job
  title: Deploy Review
  description: Review pending deploys and create summary
  max_iterations: 10
```

Execution:

1. Create a job via the scheduler
2. Run full agent loop with tools
3. Return when complete or max iterations reached

### Guardrails

Prevents runaway execution:

```yaml theme={null}
guardrails:
  cooldown: 300s           # Min time between fires
  max_concurrent: 1        # Max simultaneous runs
  dedup_window: 3600s      # Content-hash dedup for event triggers
```

### Notification Config

```yaml theme={null}
notify:
  channel: telegram        # Where to send notifications
  user: myuser             # Who to notify
  on_attention: true       # Notify when routine finds something
  on_failure: true         # Notify on errors
  on_success: false        # Notify on ROUTINE_OK
```

## Creating Routines

### Via CLI

```bash theme={null}
# Create from YAML file
ironclaw routine create --file routine.yaml

# List all routines
ironclaw routine list

# Enable/disable
ironclaw routine enable <id>
ironclaw routine disable <id>

# Fire manually
ironclaw routine fire <id>
```

### Via Tool Call

The agent can create routines:

```xml theme={null}
<RoutineCreate>
  <name>pr-check</name>
  <trigger>cron:0 9 * * MON-FRI</trigger>
  <action>
    Check GitHub PRs labeled 'urgent' and create summary in
    context/pr-summary.md
  </action>
</RoutineCreate>
```

### Programmatic API

```rust theme={null}
use ironclaw::agent::routine::{
    Routine, Trigger, RoutineAction, RoutineGuardrails, NotifyConfig
};
use std::time::Duration;

let routine = Routine {
    id: Uuid::new_v4(),
    name: "pr-check".to_string(),
    description: "Check for urgent PRs".to_string(),
    user_id: "alice".to_string(),
    enabled: true,
    trigger: Trigger::Cron {
        schedule: "0 9 * * MON-FRI".to_string(),
    },
    action: RoutineAction::Lightweight {
        prompt: "Check PRs labeled 'urgent'".to_string(),
        context_paths: vec![],
        max_tokens: 4096,
    },
    guardrails: RoutineGuardrails {
        cooldown: Duration::from_secs(300),
        max_concurrent: 1,
        dedup_window: None,
    },
    notify: NotifyConfig {
        channel: Some("telegram".to_string()),
        user: "alice".to_string(),
        on_attention: true,
        on_failure: true,
        on_success: false,
    },
    // Runtime state (managed by DB)
    last_run_at: None,
    next_fire_at: None,
    run_count: 0,
    consecutive_failures: 0,
    state: serde_json::json!({}),
    created_at: Utc::now(),
    updated_at: Utc::now(),
};

db.create_routine(&routine).await?;
```

## Runtime State

### Routine State

Routines can maintain state across runs:

```rust theme={null}
// Read previous state
let state_path = format!("routines/{}/state.md", routine_name);
let state = workspace.read(&state_path).await?;

// Update state after run
workspace.write(&state_path, new_state).await?;
```

State files are stored in `workspace/routines/{name}/state.md`.

### Run History

Every execution creates a `RoutineRun` record:

```rust theme={null}
pub struct RoutineRun {
    pub id: Uuid,
    pub routine_id: Uuid,
    pub trigger_type: String,
    pub trigger_detail: Option<String>,
    pub started_at: DateTime<Utc>,
    pub completed_at: Option<DateTime<Utc>>,
    pub status: RunStatus,  // Running | Ok | Attention | Failed
    pub result_summary: Option<String>,
    pub tokens_used: Option<i32>,
    pub job_id: Option<Uuid>,  // For full_job actions
}
```

Query run history:

```bash theme={null}
ironclaw routine runs <id> --limit 10
```

## Execution Engine

### Cron Ticker

Polls the database every N seconds for due routines:

```rust theme={null}
pub fn spawn_cron_ticker(
    engine: Arc<RoutineEngine>,
    interval: Duration,
) -> tokio::task::JoinHandle<()> {
    tokio::spawn(async move {
        let mut ticker = tokio::time::interval(interval);
        ticker.tick().await;  // Skip immediate first tick
        
        loop {
            ticker.tick().await;
            engine.check_cron_triggers().await;
        }
    })
}
```

Default interval: 30 seconds

### Event Matcher

Called synchronously from the agent main loop:

```rust theme={null}
// After handling incoming message
let fired = engine.check_event_triggers(&message).await;
if fired > 0 {
    tracing::info!("Fired {} event-triggered routines", fired);
}
```

Matching process:

1. Load event routines from cache
2. Filter by channel (if specified)
3. Match regex pattern against message content
4. Check guardrails (cooldown, concurrency, dedup)
5. Spawn execution in background task

### Guardrail Checks

#### Cooldown

```rust theme={null}
fn check_cooldown(&self, routine: &Routine) -> bool {
    if let Some(last_run) = routine.last_run_at {
        let elapsed = Utc::now().signed_duration_since(last_run);
        let cooldown = chrono::Duration::from_std(routine.guardrails.cooldown)
            .unwrap_or(chrono::Duration::seconds(300));
        if elapsed < cooldown {
            return false;
        }
    }
    true
}
```

#### Concurrency

```rust theme={null}
async fn check_concurrent(&self, routine: &Routine) -> bool {
    match self.store.count_running_routine_runs(routine.id).await {
        Ok(count) => count < routine.guardrails.max_concurrent as i64,
        Err(e) => {
            tracing::error!("Failed to check concurrent runs: {}", e);
            false
        }
    }
}
```

#### Deduplication

For event triggers, prevents firing on duplicate content:

```rust theme={null}
pub fn content_hash(content: &str) -> u64 {
    let mut hasher = DefaultHasher::new();
    content.hash(&mut hasher);
    hasher.finish()
}
```

Hashes are stored for the `dedup_window` duration.

## Notifications

Routines send notifications based on status:

```rust theme={null}
pub enum RunStatus {
    Running,    // In progress
    Ok,         // Nothing to report
    Attention,  // Needs user attention
    Failed,     // Error occurred
}
```

Notification format:

```
✅ Routine 'pr-check': ok

🔔 Routine 'pr-check': attention

Found 3 PRs labeled 'urgent':
- PR #123: Fix auth bug
- PR #124: Update deps
- PR #125: Performance fix

❌ Routine 'pr-check': failed

Error: GitHub API rate limit exceeded
```

## Database Schema

### Routines Table

```sql theme={null}
CREATE TABLE routines (
    id UUID PRIMARY KEY,
    name TEXT NOT NULL,
    description TEXT,
    user_id TEXT NOT NULL,
    enabled BOOLEAN NOT NULL DEFAULT true,
    trigger_type TEXT NOT NULL,
    trigger_config JSONB NOT NULL,
    action_type TEXT NOT NULL,
    action_config JSONB NOT NULL,
    guardrail_cooldown_seconds INTEGER NOT NULL,
    guardrail_max_concurrent INTEGER NOT NULL,
    guardrail_dedup_window_seconds INTEGER,
    notify_config JSONB NOT NULL,
    last_run_at TIMESTAMP,
    next_fire_at TIMESTAMP,
    run_count BIGINT NOT NULL DEFAULT 0,
    consecutive_failures INTEGER NOT NULL DEFAULT 0,
    state JSONB NOT NULL DEFAULT '{}',
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
```

### Routine Runs Table

```sql theme={null}
CREATE TABLE routine_runs (
    id UUID PRIMARY KEY,
    routine_id UUID NOT NULL REFERENCES routines(id) ON DELETE CASCADE,
    trigger_type TEXT NOT NULL,
    trigger_detail TEXT,
    started_at TIMESTAMP NOT NULL,
    completed_at TIMESTAMP,
    status TEXT NOT NULL,
    result_summary TEXT,
    tokens_used INTEGER,
    job_id UUID REFERENCES jobs(id),
    created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
```

## Configuration

```bash theme={null}
# Enable routines
ROUTINES_ENABLED=true

# Cron ticker interval (seconds)
ROUTINES_CRON_INTERVAL=30

# Global max concurrent routines
ROUTINES_MAX_CONCURRENT=10

# Event cache refresh interval (seconds)
ROUTINES_EVENT_CACHE_REFRESH=60
```

## Examples

### Daily PR Summary

```yaml theme={null}
name: daily-pr-summary
description: Summarize open PRs every morning
trigger:
  type: cron
  schedule: "0 9 * * MON-FRI"
action:
  type: lightweight
  prompt: |
    Check GitHub for open PRs. For each PR:
    1. Title and number
    2. Author and review status
    3. Labels and age
    
    Save summary to context/pr-summary.md
  context_paths:
    - context/priorities.md
  max_tokens: 4096
guardrails:
  cooldown: 3600s
  max_concurrent: 1
notify:
  channel: slack
  user: team
  on_attention: true
  on_failure: true
  on_success: false
```

### Deploy Webhook

```yaml theme={null}
name: deploy-webhook
description: Run tests and deploy on webhook
trigger:
  type: webhook
  secret: ${DEPLOY_SECRET}
action:
  type: full_job
  title: Deploy
  description: Run tests, build, and deploy to production
  max_iterations: 20
guardrails:
  cooldown: 60s
  max_concurrent: 1
notify:
  channel: ops
  user: oncall
  on_attention: true
  on_failure: true
  on_success: true
```

### Security Alert Monitor

```yaml theme={null}
name: security-alerts
description: Monitor for security alerts in logs
trigger:
  type: event
  channel: monitoring
  pattern: "(?i)(security|auth|breach|hack)"
action:
  type: lightweight
  prompt: |
    Analyze the security-related message.
    If it's a false positive, reply ROUTINE_OK.
    If it needs attention, summarize the threat.
  max_tokens: 2048
guardrails:
  cooldown: 300s
  max_concurrent: 3
  dedup_window: 3600s
notify:
  channel: security
  user: security-team
  on_attention: true
  on_failure: true
  on_success: false
```

## Source Code

Key files:

* `src/agent/routine.rs` - Core types (Routine, Trigger, Action, Guardrails)
* `src/agent/routine_engine.rs` - Execution engine (cron ticker, event matcher)
* `src/agent/scheduler.rs` - Job scheduling for full\_job actions
* `migrations/` - Database schema migrations
