Manual, event-triggered, webhook-triggered, and schedule-triggered automations with optional event conditions. Use when creating automations, wiring events or webhooks, running one on demand, or understanding triggers.
80%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Low
Low-risk findings worth noting
Fix and improve this skill with Tessl
tessl review fix ./.agents/skills/automations/SKILL.mdAutomations are the user-facing umbrella for agent-executed tasks that run on
demand, in response to events or webhooks, or on a cron schedule. Schedule,
Event, Manual, and Webhook are the persisted trigger types. Each
automation is a markdown resource under jobs/ with YAML frontmatter describing
when and how it fires, and a body containing natural-language instructions the
agent follows.
Recurring Jobs is the legacy name and API for scheduled automations.
manage-jobs, jobs/, and /agent#jobs remain stable compatibility surfaces.
Use manage-automations for new personal or organization automations, including
per-automation model overrides and MCP allowlists. Keep manage-jobs for
existing schedule-only integrations and delivery metadata.
| Type | Fires when | Trigger-specific fields |
|---|---|---|
schedule | Cron expression matches (same as recurring jobs) | schedule (required), timezone |
event | A matching event is emitted on the event bus | event (required), condition (optional) |
manual | A user explicitly invokes run-now | None |
webhook | A POST reaches its generated webhook endpoint | None |
Event triggers can optionally include a condition -- a natural-language
string evaluated by a model against the event payload before dispatch. If the
condition does not match, the automation is skipped. Manual automations have no
schedule, timezone, event, or condition; they never fire from the
scheduler or event bus and run only through manage-automations
action=run-now. Webhook automations run only when their generated endpoint is
called. The endpoint path contains a secret token; share it only with the
intended webhook provider.
The UI's Email received choice is not a persisted trigger type. It is a
specialized event editor that persists triggerType: event with
event: mail.message.received and represents email filters as the event
condition.
/settings/automations; open it with open-settings-page
page automations).manage-automations with
action=list-events to discover the exact registered event name and payload.manage-automations with action=define to write a
jobs/<name>.md resource.action=run-now.lastRun, lastStatus, lastError) is written back to the resource
frontmatter.Trigger acquisition stays separate by design: the scheduler decides when a cron
expression is due, the event dispatcher matches event names, owners, and
conditions, the webhook route authenticates its endpoint token, and run-now is
the only acquisition path for manual automations. Everything after a trigger is
accepted uses the same execution lifecycle.
---
enabled: true
triggerType: event
event: calendar.booking.created
condition: "attendee email ends with @example.com"
mode: agentic
domain: calendar
createdBy: user@example.com
runAs: creator
---
Send a Slack message to #sales with the booking details.
Use the web-request tool with ${keys.SLACK_WEBHOOK}.| Field | Type | Purpose |
|---|---|---|
schedule | string | Cron expression; new scheduled automations default to once per hour (0 * * * *) when omitted |
enabled | boolean | Whether the automation is active |
triggerType | "schedule" | "event" | "manual" | "webhook" | How the automation fires |
event | string? | Event name to subscribe to (event triggers only) |
condition | string? | Natural-language event condition (event triggers only) |
mode | "agentic" | Full agent loop (only supported mode; "deterministic" was removed — never implemented, rejected at define time) |
model | string? | Override the model for this trigger's agent loop |
reasoningEffort | string? | Override reasoning effort for this trigger's model; omitted uses the model's default |
domain | string? | Grouping tag (mail, calendar, clips, etc.) |
createdBy | string? | Creator email; required for organization event automations |
orgId | string? | Organization scope |
runAs | "creator" | "shared" | Execution identity; trigger-aware automations use creator |
mcpTools | string[]? | Exact MCP tool allowlist for this automation |
lastRun | string? | ISO timestamp of last execution |
lastStatus | string? | success, error, running, skipped, or paused |
lastError | string? | The last failure cause or declared skip reason |
lastErrorCode | string? | Typed code of the last failure (see Failure handling) |
consecutiveFailures | number? | Run of identical lastErrorCode failures |
pausedReason | string? | Set with enabled: false when the framework paused it |
pausedAt | string? | ISO timestamp of that pause |
All automation operations are accessed through a single manage-automations tool with an action parameter:
| Action | Purpose |
|---|---|
list-events | Discover registered event names, descriptions, and payload schemas before defining or changing an event trigger |
list | List automations and their trigger, status, model, tools, and delivery metadata |
define | Create an automation after confirming the summary with the user; requires name, trigger_type, and body |
update | Update an existing automation in its original scope without changing its creator |
delete | Delete an automation (always confirm with user first) |
fire-test | Emit a test.event.fired event to validate event automations |
run-now | Explicitly run one automation immediately with real actions and side effects |
Use list-events only when selecting an event trigger; schedule, manual, and
webhook triggers do not need event discovery. For Email received, use the
registered mail.message.received event rather than inventing an email trigger
type.
For define and update, send trigger_type: schedule with a cron schedule,
trigger_type: event with an exact registered event and optional condition,
or trigger_type: manual/webhook with none of those trigger-specific fields.
Webhook definitions return a secret endpoint path; share it only with the
intended provider. Pass the same scope when updating an existing automation.
manage-automations supports model, reasoning_effort, and mcpTools on
define/update; the MCP allowlist is enforced, not advisory.
Use run-now only for an explicit user-authorized execution. It can run any
persisted trigger type immediately, is the sole execution path for manual
automations, returns a durable run id, and does not change the next scheduled
run.
Additional tool: web-request — outbound HTTP with ${keys.NAME} substitution.
During an automation run, automation-no-op declares that no action or report
was needed. Pass a short reason (1–500 characters), such as "No urgent mail
found." The runner records skipped in the list and Past runs without counting
a failure or sending a report. This declaration cannot cover a failed tool,
rejected send, or missing credentials. Closing prose alone does not declare a
no-op: a run needs confirmed work, delivered output, or this structured signal.
manage-automations accepts personal or organization scope and supports
model, reasoning_effort, and mcpTools on define/update. An MCP allowlist
is enforced, not advisory: every named tool must resolve in the creator's
request context or the run fails clearly, and the runner never widens access
beyond the configured names.
Organization event automations are visible to organization members but always run as their creator:
createdBy is required and runAs must be creator.Integrations register events at module load time. The bus validates payloads against Standard Schema definitions and dispatches to subscribers.
import { registerEvent, emit } from "@agent-native/core/event-bus";
import { z } from "zod";
// Register an event type (typically in a server plugin)
registerEvent({
name: "calendar.booking.created",
description: "A new calendar booking was created",
payloadSchema: z.object({
bookingId: z.string(),
attendeeEmail: z.string(),
startTime: z.string(),
}),
example: { bookingId: "abc", attendeeEmail: "jane@co.com", startTime: "2025-01-15T10:00:00Z" },
});
// Emit the event (from an action, webhook handler, etc.)
emit("calendar.booking.created", {
bookingId: "abc",
attendeeEmail: "jane@co.com",
startTime: "2025-01-15T10:00:00Z",
}, { owner: "user@example.com" });| Event | Source |
|---|---|
test.event.fired | Manual / manage-automations action=fire-test |
agent.turn.completed | Agent chat |
calendar.* | Calendar integration |
clip.* | Clips integration |
mail.* | Mail integration |
| Function | Purpose |
|---|---|
registerEvent | Declare an event type with schema |
emit | Fire an event (validates payload) |
subscribe | Listen for an event (returns subscription ID) |
unsubscribe | Remove a subscription by ID |
listEvents | List all registered event definitions |
When an automation has a condition, the dispatcher resolves the owner's agent engine through the same resolveEngine path chat and the automation's own run use (owner key, deployment fallback, or Builder Gateway) and asks it a yes/no classification question. This is a yes/no classification, not a generation task; it is never hardcoded to a single provider, so an owner using Builder Gateway, OpenRouter, or any other configured provider can use conditions, not only Anthropic.
error (or a missing_credentials precondition) rather than silently skipping.web-request Tool and KeysAutomations use the web-request tool for outbound HTTP. It supports ${keys.NAME} placeholders in the URL, headers, and body. These are resolved server-side after the agent emits the tool call -- the raw secret value never enters the agent's context.
/_agent-native/secrets/adhoc API.resolveKeyReferences() resolves placeholders, falling back from user scope to workspace scope.validateUrlAllowlist() checks the resolved URL against per-key allowlists (origin-level matching).${keys.NAME} and synthetic example.com identities.An automation fails once, with its real cause, instead of re-failing every
tick. The runner classifies each failure (jobs/automation-outcome.ts):
| Class | Codes | Pauses after |
|---|---|---|
| Precondition | missing_credentials, missing_tools, owner_missing, owner_reserved, config_invalid, connection_required | 3 identical (owner/identity failures: immediately) |
| Runtime | the run's own code, e.g. http_502 | 5, with a widening gap between attempts |
lastFailedEventId): queue retries of the same event never pause it alone.agent_runs row exists: no
"Job:" thread per tick. The failure is recorded on the automation and its run
history only. Never write a generic "ended with status: errored"; surface the
run's own error.connection_required is the exception to that: a run only learns a provider
is not connected by running and yielding to the connection request, so every
failed attempt leaves a thread and an agent_runs row. It skips the runtime
backoff, pauses after 3 identical failures, and stays paused until the owner
enables the automation again; connecting the provider alone does not resume
it.enabled: false, lastStatus: paused, and
pausedReason/pausedAt. The owner is emailed once by the run that paused
it (for organization and shared jobs: the creator while still a member,
otherwise an org owner or admin). Enabling the automation clears the pause
and the streak. One paused for an absent credential (missing_credentials)
resumes by itself once its identity has a usable LLM credential; a rejected
key stays paused until the owner enables it again.running, remote
reconcile); it never moves the streak, and resumes the job only on success.automation_paused
(error_code, failure_kind, consecutive_failures, surface) and
automation_resumed (via) carry automation_hash, the first 12 hex
characters of sha256 of <app>|<name>, never the name. A failed run's
capture carries its own thread, run and automation name in
extra.failureContext.runAs: shared and organization jobs run as the organization, never as the
creator's personal connection: only an org admin can connect the provider..test, .invalid, .example, example.com|org|net) in production, are
disabled with owner_missing / owner_reserved. Absence is proven only by a
populated built-in "user" table (case-insensitive); a lookup error, or a
deployment whose accounts live elsewhere (custom getSession), is never read
as "deleted".automation_runs stuck running for over 25
hours (interrupted, automation_run_abandoned) and A2A tasks idle for 24
hours (failed, a2a_task_abandoned), a bounded batch at a time.The full-page Agent surface's Automations tab is the primary management
surface for schedule, event, manual, and webhook automations. Users can create,
edit, run now, view status, enable/disable, inspect, and delete automations
there. Its URL remains /agent#jobs for compatibility even though the visible
tab is Automations.
User: "When someone books a meeting with a @example.com email, message me in Slack."
Agent flow:
manage-automations with action=list-events to find calendar.booking.created.manage-automations with action=define:
name: slack-on-example-bookingtrigger_type: eventevent: calendar.booking.createdcondition: attendee email ends with @example.commode: agenticdomain: calendarbody: Send a Slack message to #sales with the booking details. Use the web-request tool to POST to ${keys.SLACK_WEBHOOK}.| File | Purpose |
|---|---|
packages/core/src/triggers/types.ts | TriggerFrontmatter interface |
packages/core/src/triggers/actions.ts | Agent tools (define, list, update, delete, test) |
packages/core/src/triggers/dispatcher.ts | Event subscription and agentic dispatch |
packages/core/src/jobs/background-automation-runner.ts | Shared schedule/event/manual/webhook execution lifecycle |
packages/core/src/jobs/automation-outcome.ts | Failure classification, pause thresholds, reserved identities |
packages/core/src/jobs/stale-reaper.ts | Bounded reaping of stuck runs and A2A tasks |
packages/core/src/triggers/condition-evaluator.ts | Engine-resolved condition classification with caching |
packages/core/src/event-bus/ | Event bus (register, emit, subscribe) |
packages/core/src/tools/fetch-tool.ts | web-request tool with key substitution |
packages/core/src/secrets/substitution.ts | resolveKeyReferences() and validateUrlAllowlist() |
recurring-jobs -- schedule-triggered automations reuse the same schedulersecrets -- ad-hoc keys and ${keys.NAME} substitutionactions -- automations can call any registered action via the agent loopdelegate-to-agent -- agentic mode runs a full runAgentLoopBuilderIO/agent-native
4712315
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.