Skip to content

Automations

Automations let you trigger actions automatically when events happen in Craft Agent. You can run shell commands, send prompts to start new sessions, or schedule recurring workflows — all configured in a single JSON file.

The easiest way to set up automations is to describe what you want in plain language. Here are some prompts you can try:

What you wantWhat to say
Scheduled briefing“Set up a daily standup briefing every weekday at 9am”
Desktop notifications“Notify me with a macOS notification when a session is labelled urgent”
Audit logging“Log all permission mode changes to a file”
Auto-label“When a session starts, run a command that checks the working directory and logs it”
Recurring report“Every Friday at 5pm, create a session that summarises this week’s completed tasks”
Webhook integration“When I flag a session, send a curl request to my webhook URL”

Here’s the simplest possible automation — it logs a message every time a label is added to a session:

{
"version": 2,
"automations": {
"LabelAdd": [
{
"actions": [
{ "type": "command", "command": "echo \"Label added: $CRAFT_LABEL\" >> ~/craft-automations.log" }
]
}
]
}
}

Save this as ~/.craft-agent/workspaces/{workspaceId}/automations.json and it starts working immediately — no restart needed.

Want Craft Agent to do something for you on a schedule? Use a prompt action with a cron expression:

{
"version": 2,
"automations": {
"SchedulerTick": [
{
"cron": "0 9 * * 1-5",
"timezone": "America/New_York",
"labels": ["Scheduled"],
"actions": [
{ "type": "prompt", "prompt": "Check @github for any new issues assigned to me and summarise them" }
]
}
]
}
}

This creates a new session every weekday at 9 AM, activates the GitHub source, and asks the agent to check your issues. The session is automatically labelled “Scheduled” for easy filtering.

When an event fires (e.g., a label is added, a tool runs, or a cron schedule matches), Craft Agent checks your configuration for matching entries and executes them.

There are two types of actions:

  • Command actions — execute shell commands with event data available as environment variables
  • Prompt actions — send a prompt to Craft Agent, creating a new session (App events only)

Automations are configured per-workspace in automations.json:

~/.craft-agent/workspaces/{workspaceId}/automations.json
{
"version": 2,
"automations": {
"EventName": [
{
"matcher": "regex-pattern",
"actions": [
{ "type": "command", "command": "echo 'Hello'" }
]
}
]
}
}

Each event type maps to an array of matchers, and each matcher contains an array of actions to execute when the matcher’s pattern matches the event value.

Automations are listed in the sidebar under Automations. From here you can:

  • Enable / Disable — Toggle individual automations on or off without removing them
  • Duplicate — Create a copy of an existing automation with a “Copy” suffix
  • Delete — Remove an automation permanently
  • Test — Manually trigger an automation to verify it works before waiting for the real event. The test runner resolves @mentions, enables sources, and uses the configured llmConnection/model — the same code path as the scheduler.
  • Execution history — Each automation records success and failure to a timeline, viewable in the detail page. History is stored in automations-history.jsonl and retained to the last 20 runs per automation (max 1000 entries total).

Batch operations are supported — select multiple automations and enable, disable, or delete them together. The list is sorted by most recent execution for quick access to active automations.

You can also manage automations by editing automations.json directly. Changes take effect immediately — no restart required.

Enabling and disabling: Set "enabled": false on any matcher to temporarily disable it without removing the configuration. Omit the field or set it to true to re-enable.

{
"matcher": "^urgent$",
"enabled": false,
"actions": [
{ "type": "command", "command": "notify-send 'Urgent!'" }
]
}

Deleting: Remove the matcher entry from automations.json. If an event type has no remaining matchers, you can remove the entire event key.

Triggered by Craft Agent itself. Support both command and prompt actions.

EventTriggerMatch Value
LabelAddLabel added to sessionLabel ID (e.g., urgent)
LabelRemoveLabel removed from sessionLabel ID
LabelConfigChangeLabel configuration changedAlways matches
PermissionModeChangePermission mode changedNew mode name
FlagChangeSession flagged/unflaggedtrue or false
SessionStatusChangeSession status changedNew status (e.g., done)
SchedulerTickRuns every minuteUses cron matching

Passed to the Claude SDK. Only command actions are supported (no prompt actions).

EventTriggerMatch Value
PreToolUseBefore a tool executesTool name
PostToolUseAfter a tool succeedsTool name
PostToolUseFailureAfter a tool failsTool name
UserPromptSubmitUser submits a promptEvent data JSON
SessionStartSession startsEvent data JSON
SessionEndSession endsEvent data JSON
StopAgent stopsEvent data JSON
SubagentStartSubagent spawnedEvent data JSON
SubagentStopSubagent completesEvent data JSON
NotificationNotification receivedEvent data JSON
PreCompactBefore context compactionEvent data JSON
PermissionRequestPermission requestedEvent data JSON
SetupAgent setup/initializationEvent data JSON

Execute a shell command when the event fires. Event data is available through environment variables.

{
"type": "command",
"command": "echo \"Label $CRAFT_LABEL was added\" >> ~/automation-log.txt",
"timeout": 60000
}
PropertyTypeDefaultDescription
type"command"RequiredAction type
commandstringRequiredShell command to execute
timeoutnumber60000Timeout in milliseconds

Send a prompt to Craft Agent, creating a new session. Only works with App events.

{
"type": "prompt",
"prompt": "Run the @weather skill and summarize today's forecast"
}
PropertyTypeDefaultDescription
type"prompt"RequiredAction type
promptstringRequiredPrompt text to send
llmConnectionstringWorkspace defaultLLM connection slug (configured in AI Settings)
modelstringWorkspace defaultModel ID for the created session
thinkingLevel"off" | "low" | "medium" | "high" | "xhigh" | "max"Workspace defaultThinking level for the spawned session

Features:

  • Use @mentions to reference sources or skills (e.g., @github, @linear)
  • Environment variables are expanded (e.g., $CRAFT_LABEL, ${CRAFT_SESSION_NAME})
  • Mentioned sources are automatically activated for the new session

Per-Action Overrides: You can override the AI provider, model, and thinking level for the session created by a prompt action. Each field is independent — omit any of them to inherit the workspace default.

{
"type": "prompt",
"prompt": "Quick code review of recent changes",
"llmConnection": "my-copilot-connection",
"model": "gemini-2.5-flash",
"thinkingLevel": "high"
}

The llmConnection value is the slug of an LLM connection configured in AI Settings. The model value is a model ID supported by the provider. If either is invalid or not found, it gracefully falls back to the workspace default.

Thinking level resolution: thinkingLevel on the action takes precedence; if omitted, the workspace default applies; if neither is set, sessions default to "medium". The legacy "think" value from older configs is silently migrated to "medium". Backends gracefully cap unsupported values (e.g. Anthropic auto-degrades xhighhigh on models that don’t support it; Pi caps max at xhigh).

Most events use regex matching against the event’s match value:

{
"matcher": "^urgent$",
"actions": [
{ "type": "command", "command": "notify-send 'Urgent!'" }
]
}

Omit the matcher field to match all events of that type.

Examples:

PatternMatches
^urgent$Exact match for “urgent”
bug|featureEither “bug” or “feature”
^prod-Any value starting with “prod-”
(omitted)All events of that type

For SchedulerTick events, use cron expressions instead of regex:

{
"cron": "0 9 * * 1-5",
"timezone": "Europe/Budapest",
"actions": [
{ "type": "prompt", "prompt": "Give me a morning briefing" }
]
}

Cron format: minute hour day-of-month month day-of-week

FieldRangeExamples
Minute0–590, */15
Hour0–239, 14
Day of month1–311, 15
Month1–121, */3
Day of week0–6 (0 = Sunday)1-5, 0,6

Common patterns:

CronSchedule
*/15 * * * *Every 15 minutes
0 9 * * *Daily at 9:00 AM
0 9 * * 1-5Weekdays at 9:00 AM
30 14 1 * *1st of each month at 2:30 PM
0 */6 * * *Every 6 hours

Timezone: Uses IANA timezone names (e.g., Europe/Budapest, America/New_York). Defaults to system timezone if not specified. Verify with crontab.guru.

Conditions are optional filters that run after the matcher/cron matches but before actions fire. All conditions in the array must pass (implicit AND). If the array is empty or omitted, actions fire unconditionally.

{
"cron": "0 9 * * *",
"timezone": "Europe/Budapest",
"conditions": [
{
"condition": "time",
"weekday": ["mon", "tue", "wed", "thu", "fri"]
}
],
"actions": [
{ "type": "prompt", "prompt": "Good morning! Here's your daily briefing." }
]
}

Check time-of-day and day-of-week in a given timezone.

{
"condition": "time",
"after": "09:00",
"before": "17:00",
"weekday": ["mon", "tue", "wed", "thu", "fri"],
"timezone": "Europe/Budapest"
}
PropertyTypeDescription
after"HH:MM"Start of time window (inclusive)
before"HH:MM"End of time window (exclusive)
weekdaystring[]Allowed days: mon, tue, wed, thu, fri, sat, sun
timezonestringIANA timezone. Falls back to matcher timezone, then system local

Check fields from the event payload. Useful for filtering on specific transitions or values.

{
"condition": "state",
"field": "permissionMode",
"from": "safe",
"to": "allow-all"
}
PropertyTypeDescription
fieldstringPayload field name (e.g., permissionMode, sessionStatus, labels, isFlagged)
valueanyExact match
fromanyPrevious value (for transition events)
toanyNew value (for transition events)
containsstringArray membership check (e.g., check if a label is present)
not_valueanyMatches anything except this value

Combine conditions with and, or, and not:

{
"condition": "and",
"conditions": [
{ "condition": "time", "weekday": ["mon", "tue", "wed", "thu", "fri"] },
{ "condition": "time", "after": "09:00", "before": "17:00" }
]
}
{
"condition": "or",
"conditions": [
{ "condition": "state", "field": "permissionMode", "value": "allow-all" },
{ "condition": "state", "field": "isFlagged", "value": true }
]
}
{
"condition": "not",
"conditions": [
{ "condition": "time", "weekday": ["sat", "sun"] }
]
}
TypeBehaviour
andAll sub-conditions must pass
orAt least one sub-condition must pass
notNone of the sub-conditions may pass

Each matcher entry supports these optional fields:

PropertyTypeDefaultDescription
matcherstringMatch allRegex pattern for event filtering
cronstringCron expression (SchedulerTick only)
timezonestringSystem TZIANA timezone for cron
conditionsarray[]Conditions that must all pass before actions fire (see Conditions)
permissionModestring"safe"Security mode for commands
labelsstring[][]Labels applied to prompt-created sessions
telegramTopicstringRoutes spawned sessions to a Telegram forum topic — see Telegram Topic Routing
enabledbooleantrueSet to false to disable without deleting
actionsarrayRequiredArray of command/prompt actions

When you have a Telegram supergroup paired in Settings → Messaging → Telegram, you can route the sessions a matcher spawns into a dedicated forum topic by setting the telegramTopic field.

{
"automations": {
"LabelAdd": [
{
"matcher": "^urgent$",
"telegramTopic": "Urgent Alerts",
"permissionMode": "ask",
"actions": [
{ "type": "prompt", "prompt": "Look at the urgent issue: $LABEL" }
]
}
]
}
}

When the matcher fires, the spawned session is bound to a forum topic of the given name. The topic is created on first use and reused thereafter, so subsequent runs of the same matcher (and any other matcher that uses the same telegramTopic value) post into the same topic.

All of these must hold; otherwise the field is silently ignored and the session runs without a Telegram binding (same as if the field were absent):

  • A Telegram supergroup is paired at Settings → Messaging → Telegram
  • The Telegram bot is connected
  • The bot has the Manage Topics admin permission in the supergroup

If you haven’t paired a supergroup yet, follow these steps. The whole flow takes about a minute.

1. Create a supergroup with topics enabled

Section titled “1. Create a supergroup with topics enabled”

A regular Telegram group can’t host topics — you need a supergroup with “Topics” mode enabled.

  • New group: Telegram → New Group → add anyone (or yourself for a solo workspace) → set a name. Telegram converts groups to supergroups automatically once members exceed a threshold or you flip Topics on.
  • Enable Topics: Open the group → tap the group name → Edit (pencil icon on mobile, ⋯ on desktop) → toggle Topics on → Save. The group is now a forum supergroup; you’ll see a “General” topic and a ”+” button to create more.

Open the supergroup → tap the group name → Add members → search for your bot’s username (e.g. @CraftAgentsBot) → add it.

3. Make the bot an admin with “Manage Topics”

Section titled “3. Make the bot an admin with “Manage Topics””

This is the step most people miss — the bot needs explicit permission to create new topics, otherwise topic-creation calls return 400: Bad Request: not enough rights to create a topic.

  1. In the supergroup, tap the group name → EditAdministrators.
  2. Tap Add Administrator → pick the bot.
  3. In the permissions list, toggle on Manage Topics (other permissions like Delete Messages or Pin Messages are optional and not required by the topic-routing feature).
  4. Tap Save / Done.
  1. In the Craft Agent app: Settings → Messaging → Telegram → Pair Supergroup.
  2. The dialog shows a 6-digit code, a deep link to the bot, and a 5-minute countdown.
  3. In Telegram, in any topic of your supergroup, type /pair <code> (replace <code> with the digits from the dialog).
  4. The bot replies with a confirmation. The dialog closes automatically and the Settings row updates with the supergroup’s title and chat ID.

That’s it — automations with telegramTopic set will now create topics in this supergroup.

  • Topic names are 1–128 characters and case-sensitive ("Reports" and "reports" are different topics).
  • Two matchers using the same telegramTopic value share one topic — useful for grouping related automations.
  • If you change a matcher’s telegramTopic value, the next run uses (or creates) the new topic; the old topic remains in Telegram with its history intact.
  • Renaming the topic in Telegram doesn’t sync back — the binding follows the topic ID, not the display name.

Available to all command actions:

VariableDescription
CRAFT_EVENTEvent name (e.g., LabelAdd, PreToolUse)
CRAFT_EVENT_DATAFull event payload as JSON
CRAFT_SESSION_IDCurrent session ID
CRAFT_SESSION_NAMECurrent session name
CRAFT_WORKSPACE_IDCurrent workspace ID

Dynamically generated from event payloads:

Label events (LabelAdd, LabelRemove):

  • CRAFT_LABEL — Label ID being added/removed

PermissionModeChange:

  • CRAFT_OLD_MODE — Previous permission mode
  • CRAFT_NEW_MODE — New permission mode

FlagChange:

  • CRAFT_IS_FLAGGEDtrue or false

SessionStatusChange:

  • CRAFT_OLD_STATE — Previous session status
  • CRAFT_NEW_STATE — New session status

SchedulerTick:

  • CRAFT_LOCAL_TIME — Current time (HH:MM)
  • CRAFT_LOCAL_DATE — Current date (YYYY-MM-DD)

Tool events (PreToolUse, PostToolUse, PostToolUseFailure):

  • CRAFT_TOOL_NAME — Tool being used
  • CRAFT_TOOL_INPUT — Tool parameters as JSON
  • CRAFT_TOOL_RESPONSE — Tool output (PostToolUse only)
  • CRAFT_ERROR — Error message (PostToolUseFailure only)

Session events (SessionStart):

  • CRAFT_SOURCE — Session source (e.g., startup, resume)
  • CRAFT_MODEL — Model name

Subagent events (SubagentStart, SubagentStop):

  • CRAFT_AGENT_ID — Subagent ID
  • CRAFT_AGENT_TYPE — Subagent type

Command actions run with security checks by default. You can adjust this per matcher:

ModeBehaviourUse Case
safeCommands checked against allowlistDefault, recommended
allow-allBypass security checksTrusted automation only
{
"matcher": "^urgent$",
"permissionMode": "allow-all",
"actions": [
{ "type": "command", "command": "osascript -e 'display notification \"Urgent!\" with title \"Craft Agent\"'" }
]
}

Prompt actions can attach labels to the sessions they create. This makes it easy to filter and organise scheduled sessions:

{
"cron": "0 9 * * *",
"labels": ["Scheduled", "morning-briefing"],
"actions": [
{ "type": "prompt", "prompt": "Give me today's priorities" }
]
}

Labels also support environment variable expansion: "priority::${CRAFT_LABEL}".

To prevent runaway loops (e.g., an automation that indirectly triggers itself), the event bus enforces rate limits:

EventMax fires / minute
SchedulerTick60 (1/sec)
All other events10

Excess events are silently dropped for the remainder of the 60-second window.

Schedule a prompt every weekday at 9 AM:

{
"version": 2,
"automations": {
"SchedulerTick": [
{
"cron": "0 9 * * 1-5",
"timezone": "Europe/Budapest",
"labels": ["Scheduled", "briefing"],
"actions": [
{ "type": "prompt", "prompt": "Run the @daily-standup skill" }
]
}
]
}
}

Use a time condition to restrict a daily schedule to weekdays:

{
"version": 2,
"automations": {
"SchedulerTick": [
{
"name": "Morning AI news",
"cron": "0 9 * * *",
"timezone": "Europe/Budapest",
"conditions": [
{
"condition": "time",
"weekday": ["mon", "tue", "wed", "thu", "fri"],
"timezone": "Europe/Budapest"
}
],
"labels": ["Scheduled", "ai-news"],
"actions": [
{ "type": "prompt", "prompt": "Run the @ai-news skill and summarize today's AI developments" }
]
}
]
}
}

Only notify when permission mode changes specifically from safe to allow-all:

{
"version": 2,
"automations": {
"PermissionModeChange": [
{
"conditions": [
{
"condition": "state",
"field": "permissionMode",
"from": "safe",
"to": "allow-all"
}
],
"actions": [
{ "type": "command", "command": "osascript -e 'display notification \"Permission escalated to allow-all\" with title \"Craft Agent\"'" }
]
}
]
}
}

Track when labels are added or removed:

{
"version": 2,
"automations": {
"LabelAdd": [
{
"permissionMode": "allow-all",
"actions": [
{ "type": "command", "command": "echo \"[$(date)] Added: $CRAFT_LABEL\" >> ~/label-log.txt" }
]
}
],
"LabelRemove": [
{
"permissionMode": "allow-all",
"actions": [
{ "type": "command", "command": "echo \"[$(date)] Removed: $CRAFT_LABEL\" >> ~/label-log.txt" }
]
}
]
}
}
{
"version": 2,
"automations": {
"LabelAdd": [
{
"matcher": "^urgent$",
"permissionMode": "allow-all",
"actions": [
{
"type": "command",
"command": "osascript -e 'display notification \"Urgent session flagged\" with title \"Craft Agent\"'"
}
]
}
]
}
}
{
"version": 2,
"automations": {
"PermissionModeChange": [
{
"permissionMode": "allow-all",
"actions": [
{
"type": "command",
"command": "echo \"$(date): $CRAFT_OLD_MODE -> $CRAFT_NEW_MODE\" >> ~/mode-audit.log"
}
]
}
]
}
}

Run different automations at different times, and disable some without deleting:

{
"version": 2,
"automations": {
"SchedulerTick": [
{
"cron": "0 9 * * 1-5",
"timezone": "Europe/Budapest",
"labels": ["Scheduled"],
"actions": [
{ "type": "prompt", "prompt": "Give me a morning briefing" }
]
},
{
"cron": "0 17 * * 5",
"timezone": "Europe/Budapest",
"enabled": false,
"labels": ["Scheduled"],
"actions": [
{ "type": "prompt", "prompt": "Summarise this week's completed tasks" }
]
}
]
}
}

The second automation (Friday summary) is disabled and won’t run until "enabled" is set to true or removed.

Prompt with Custom Connection, Model, and Thinking Level

Section titled “Prompt with Custom Connection, Model, and Thinking Level”

Override provider, model, and thinking level for a specific automation:

{
"version": 2,
"automations": {
"SchedulerTick": [
{
"cron": "0 8 * * 1-5",
"timezone": "Europe/Budapest",
"labels": ["Scheduled"],
"actions": [
{
"type": "prompt",
"prompt": "Review overnight @github pull requests",
"llmConnection": "openrouter",
"model": "anthropic/claude-sonnet-4.6",
"thinkingLevel": "high"
}
]
}
]
}
}

Ask Craft Agent to validate your automations:

Validate my automations configuration

Or use the config_validate tool with target: "all".

The validator checks for:

  • Invalid JSON syntax
  • Unknown event names
  • Empty actions arrays
  • Invalid cron expressions or timezones
  • Invalid or unsafe regex patterns (ReDoS prevention)
  • References to non-existent labels
  • Invalid condition types, field names, or weekday values
  • Condition nesting depth exceeding limits
  • Missing llmConnection slugs (error — will fail at runtime)
  • Model/provider mismatches when both llmConnection and model are specified (warning)
  • Invalid thinkingLevel values (legacy "think" is silently migrated to "medium")

Automations include several built-in safety measures:

  • Shell injection prevention — User-controlled values in environment variables (session names, labels, prompts) are automatically escaped
  • ReDoS protection — Regex patterns are limited to 500 characters and checked for catastrophic backtracking patterns
  • Rate limiting — Prevents infinite loops from automations that trigger other automations
  • Permission modes — Commands are checked against an allowlist by default
  • Timeouts — Commands are killed after their timeout expires (with SIGKILL fallback)
Best Practices
  • Start simple — Test with echo commands before writing complex scripts
  • Use labels — Tag scheduled sessions for easy filtering
  • Set timeouts — Prevent runaway commands with the timeout field
  • Log failures — Redirect stderr to track issues: command 2>> ~/automation-errors.log
  • Be specific — Use matchers to avoid triggering on every event
  • Test cron — Use crontab.guru to verify expressions
  • Use enabled: false — Temporarily disable automations instead of deleting them during debugging
Troubleshooting: Automation not firing
  1. Check event name — Must be exact (e.g., LabelAdd not labeladd)
  2. Check matcher — Regex must match the event’s match value
  3. Check cron — For SchedulerTick, verify your cron expression
  4. Check enabled — Ensure the matcher doesn’t have "enabled": false
  5. Check logs — Look for [automations] in the application logs
Troubleshooting: Command blocked

If you see “Bash command blocked” errors:

  1. Add "permissionMode": "allow-all" to the matcher
  2. Or simplify the command to avoid shell constructs like $()
Troubleshooting: Prompt not creating session
  1. Ensure the event is an App event (prompt actions don’t work with Agent events)
  2. Check that the prompt text is not empty
  3. Verify @mentions reference valid sources or skills
Troubleshooting: Condition not matching
  1. Check weekday spelling — Must be 3-letter lowercase: mon, tue, wed, thu, fri, sat, sun
  2. Check timezone — Use IANA names (e.g., Europe/Budapest). Invalid timezones silently fall back to system local
  3. Check time format — Must be HH:MM in 24-hour format (e.g., 09:00, not 9:00 AM)
  4. Check state field names — Use permissionMode, sessionStatus, labels, isFlagged
  5. Check nesting depth — Conditions nested deeper than 8 levels always evaluate to false
  6. Check logs — Look for [automations] entries that mention condition evaluation