Skip to main content

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.

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

┌──────────┐     ┌─────────┐     ┌──────────────────┐
│  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:
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:
trigger:
  type: event
  channel: telegram  # Optional filter
  pattern: "deploy\\s+\\w+"  # Regex pattern

Webhook Trigger

Fires on incoming HTTP POST:
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:
trigger:
  type: manual

Actions

Lightweight Action

Single LLM call, no tools. Fast and cheap:
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:
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:
guardrails:
  cooldown: 300s           # Min time between fires
  max_concurrent: 1        # Max simultaneous runs
  dedup_window: 3600s      # Content-hash dedup for event triggers

Notification Config

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

# 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:
<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

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:
// 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:
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:
ironclaw routine runs <id> --limit 10

Execution Engine

Cron Ticker

Polls the database every N seconds for due routines:
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:
// 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

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

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:
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:
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

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

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

# 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

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

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

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

Build docs developers (and LLMs) love