Understanding the Automation Event System and Cron-Based Triggers in Craft Agents

Craft Agents’ automation event system is a publish/subscribe architecture that executes actions when specific events occur, using cron-based triggers exclusively through the SchedulerTick event to run scheduled automations.

The lukilabs/craft-agents-oss repository implements this system to orchestrate prompts, webhooks, and other actions based on both application-level events and time-based schedules. The core implementation spans the packages/shared/src/automations and packages/shared/src/scheduler directories, providing a type-safe, rate-limited mechanism for event-driven automation.

Core Components of the Automation Event System

Event Taxonomy

The system recognizes three event categories defined in packages/shared/src/automations/types.ts:

  • AppEvent: Native Craft events such as LabelAdd, PermissionModeChange, SessionStatusChange, and SchedulerTick
  • AgentEvent: Claude SDK events including PreToolUse, PostToolUse, Notification, and UserPromptSubmit
  • AutomationEvent: The union type AppEvent | AgentEvent used throughout the automation engine

The SchedulerTick event is the only AppEvent that supports cron-based scheduling, serving as the temporal heartbeat for the entire system.

The WorkspaceEventBus

All events flow through the WorkspaceEventBus class in packages/shared/src/automations/event-bus.ts. This publish/subscribe hub exposes two primary methods:

  • bus.on(event, handler) – Registers an event handler
  • bus.emit(event, payload) – Distributes events to registered handlers

The bus enforces rate limits per event type. Specifically, SchedulerTick is capped at 60 events per minute to prevent runaway execution, as implemented in lines 86-92 of event-bus.ts.

How Cron-Based Triggers Work

The SchedulerTick Event

The SchedulerService in packages/shared/src/scheduler/scheduler-service.ts generates the SchedulerTick event. Upon startup, it aligns to the next minute boundary and emits a payload every 60 seconds containing:

  • UTC timestamp
  • Local time string
  • Hour, minute, and day-of-week values

The AutomationSystem instantiates SchedulerService and forwards each tick to the WorkspaceEventBus, as shown in lines 292-294 of packages/shared/src/automations/automation-system.ts.

Cron Expression Matching

Cron evaluation occurs in packages/shared/src/automations/cron-matcher.ts via the matchesCron function. This utility:

  1. Parses the cron expression using the croner library
  2. Checks if the current minute matches the schedule
  3. Supports optional IANA timezone strings

The function signature is:

export function matchesCron(cronExpression: string, timezone?: string): boolean

matchesCron is re-exported publicly in packages/shared/src/automations/index.ts (lines 102-104) for use in custom automation logic.

Validation and Constraints

The validation layer in packages/shared/src/automations/validation.ts enforces a critical constraint: cron expressions are only valid for SchedulerTick events. If a configuration includes a cron field on any other event type, the validator rejects it with a descriptive warning (lines 163-170).

Additionally, the AutomationMatcher type definition in types.ts (lines 150-155) specifies that the cron field must accompany a SchedulerTick matcher, ensuring type safety across the codebase.

Implementation Examples

Configuring a Cron-Based Automation

Define a weekday morning automation in your automations.json:

{
  "automations": {
    "SchedulerTick": [
      {
        "id": "daily001",
        "name": "Morning Summary",
        "cron": "0 9 * * 1-5",
        "timezone": "Europe/Budapest",
        "actions": [
          { "type": "prompt", "prompt": "Generate the daily summary for today." }
        ]
      }
    ]
  }
}

The cron field "0 9 * * 1-5" executes at 09:00 on weekdays only.

Programmatic Cron Checking

Use matchesCron in custom extensions:

import { matchesCron } from '@craft-agents/shared/automations';

// Check if current time matches "every hour at minute 15"
if (matchesCron('15 * * * *')) {
  console.log('Executing hourly task at 15 minutes past the hour');
}

Subscribing to SchedulerTick Events

Monitor system ticks for logging or metrics:

import { WorkspaceEventBus } from '@craft-agents/shared/automations';
import type { SchedulerTickPayload } from '@craft-agents/shared/scheduler';

const bus = new WorkspaceEventBus();

bus.on('SchedulerTick', async (payload: SchedulerTickPayload) => {
  console.log(`[${payload.localTime}] Tick received - Day: ${payload.dayName}`);
});

Remember that the bus enforces a 60 events per minute limit on SchedulerTick to prevent resource exhaustion.

Summary

  • The automation event system in Craft Agents uses a publish/subscribe architecture centered on the WorkspaceEventBus to route events to registered handlers.
  • Cron-based triggers are exclusively tied to the SchedulerTick event, emitted every minute by the SchedulerService with full timestamp and calendar data.
  • The matchesCron function in cron-matcher.ts evaluates cron expressions against the current tick, supporting IANA timezones for geographically aware scheduling.
  • Validation in validation.ts strictly prevents cron expressions on non-scheduler events, ensuring architectural consistency.
  • Rate limiting on SchedulerTick (60 events per minute) protects the system from runaway automation loops.

Frequently Asked Questions

Can I use cron expressions with events other than SchedulerTick?

No. The validation layer in packages/shared/src/automations/validation.ts explicitly rejects configurations that include a cron field on any event type other than SchedulerTick. This design ensures that time-based scheduling only occurs through the dedicated minute-granular heartbeat provided by the SchedulerService.

How does the system handle timezone specifications in cron expressions?

The matchesCron function accepts an optional IANA timezone string (e.g., "America/New_York" or "Europe/Budapest"). When provided, the function evaluates the cron expression against the local time in that timezone rather than UTC. This allows automations to fire at consistent local times regardless of daylight saving transitions or server location.

What happens if an automation takes longer than a minute to execute?

The SchedulerService emits SchedulerTick events strictly every 60 seconds, and the WorkspaceEventBus enforces a rate limit of 60 ticks per minute. However, the system does not wait for previous automation actions to complete before emitting the next tick. Long-running actions may overlap with subsequent ticks, so automations should be designed to be idempotent or include their own concurrency controls if overlapping execution would cause issues.

Where is the cron matching logic implemented and can I use it independently?

The cron matching logic resides in packages/shared/src/automations/cron-matcher.ts within the matchesCron function. This utility is re-exported publicly from packages/shared/src/automations/index.ts, making it available for import as matchesCron from the @craft-agents/shared/automations package. You can use it independently to check if the current time matches any cron expression, with optional timezone support.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →