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, andSchedulerTick - AgentEvent: Claude SDK events including
PreToolUse,PostToolUse,Notification, andUserPromptSubmit - AutomationEvent: The union type
AppEvent | AgentEventused 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 handlerbus.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:
- Parses the cron expression using the
cronerlibrary - Checks if the current minute matches the schedule
- 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
WorkspaceEventBusto route events to registered handlers. - Cron-based triggers are exclusively tied to the
SchedulerTickevent, emitted every minute by theSchedulerServicewith full timestamp and calendar data. - The
matchesCronfunction incron-matcher.tsevaluates cron expressions against the current tick, supporting IANA timezones for geographically aware scheduling. - Validation in
validation.tsstrictly 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →