From ano-skills
Build, edit, run, and manage Ano automations via the `ano` CLI. Covers scheduled jobs, message-match / mention / channel-event / webhook triggers, the 5-action vocabulary (send_message, send_dm, sql_query, http_request, run_skill), template chaining, run caps, third-party OAuth connections (Linear, Gmail, Notion, PostHog, HubSpot, etc.), and the Build-Before-Talk methodology that submits a compiled plan in one call. Self-contained.
How this skill is triggered — by the user, by Claude, or both
Slash command
/ano-skills:ano-automations [command] [args...][command] [args...]The summary Claude sees in its skill listing — used to decide when to auto-load this skill
- Output: `--agent` for raw JSON, `--json` for envelope. Never parse styled TTY.
--agent for raw JSON, --json for envelope. Never parse styled TTY.ano channels list --agent / ano users list --agent resolve names.create-compiled. Do NOT iterate via automation compile — that's an LLM round-trip per revision.Building or managing an automation?
├── NEW automation (you are Claude Code) → BUILD-BEFORE-TALK below ↓
│ 0. ensure a workspace is set: `ano workspaces list --agent`,
│ then `ano workspaces use <id>` if no active workspace.
│ Without this, create-compiled refuses with a clear error.
│ 1. resolve named users/channels via ano user_get_by_name / channel_get_by_name
│ 2. compose the compiled plan offline (see Steps 1–5 below)
│ 3. validate locally → ano automation validate --file plan.json --agent
│ 4. submit once → ano automation create-compiled --file - --agent
├── Edit an existing one → ano automation update <slug-or-id> --name "..." --agent
├── List existing → ano automation list --agent
├── See recent runs → ano automation runs <slug-or-id> --agent
├── Test before enabling → ano automation run <slug-or-id> --agent (dry-run)
├── Fire for real once → ano automation run <slug-or-id> --no-dry-run --agent
├── Pause / resume → ano automation pause|resume <slug-or-id>
├── Webhook trigger setup → ano automation webhook-setup <slug-or-id> --agent
│ then activate to start firing → ano automation activate <slug-or-id> --agent
├── Needs a third-party service → ano integrations connect <app> --agent (CLI v2.9.0+)
│ (Linear, Gmail, Notion, PostHog, HubSpot, etc.)
└── Delete (irreversible) → ano automation delete <slug-or-id>
| Task | Command |
|---|---|
| Submit a compiled plan | ano automation create-compiled --file plan.json --agent |
| Submit from stdin | cat plan.json | ano automation create-compiled --file - --agent |
| Validate offline first | ano automation validate --file plan.json --agent |
| Compile only (no save) | ano automation compile "prompt" --agent |
| Create from prompt (server LLM, slow) | ano automation create "prompt" --agent |
| Update | ano automation update <slug-or-id> --name "..." --agent |
| List (returns slug + id) | ano automation list --agent |
| Recent runs | ano automation runs <slug-or-id> --agent |
| Test (dry-run) | ano automation run <slug-or-id> --agent |
| Run for real | ano automation run <slug-or-id> --no-dry-run --agent |
| Pause / Resume | ano automation pause|resume <slug-or-id> |
| Delete | ano automation delete <slug-or-id> |
| Webhook setup/rotate | ano automation webhook-setup <slug-or-id> --agent |
| Activate webhook (stub → live) | ano automation activate <slug-or-id> --agent (CLI v2.8.2+) |
ano automation list returns both slug (e.g. quiet-otter-42) and id (UUID). Every command that takes an automation positional accepts either.
--json consumers: prefer the raw id (slugs are display-only).Slugs are derived deterministically from the UUID client-side. Ambiguous slug (rare) → CLI exits non-zero with matches printed.
ano new automation / ano edit automation from BashThese spawn a child claude subprocess for a multi-turn interview. From inside a Claude Code Bash call, each invocation is a fresh subprocess with no shared state — the user answers questions to a session that immediately exits and a new session asks the same questions again.
If you are Claude Code, you ARE the agent. Use Build-Before-Talk below: compose the spec yourself, then create-compiled --file - --agent in one shot. Sub-100ms server latency, no LLM round-trip, no nested-session state loss.
For edits: ano automation update <id> --field value --agent (single-shot) OR re-compose + create-compiled.
The submission command:
ano automation create-compiled --file - --agent <<'EOF'
{ ...the full plan JSON, schema below... }
EOF
Returns {automation_id, name, enabled: false, ...}. Lands in unconfirmed state — user approves it via the Automations page or DM.
ano channels list --agent # channel IDs (note is_public, has_guests)
ano users list --agent # user IDs + emails for send_dm
ano workspaces list --agent # if multi-workspace, confirm which one
If the user mentions a coworker by name ("post as Maya"), look at workspace coworkers. If the user mentions a SQL/HTTP integration ("query our prod Postgres"), check whether the connection exists — if not, still emit the action; Ano surfaces a phantom Connect chip on the Automations page.
trigger_type | trigger_config shape | When |
|---|---|---|
schedule | { cron: "0 9 * * 1-5", tz: "Europe/Stockholm" } | Time-based (every weekday 9am, hourly, etc.) |
message_match | { channel_id, pattern, sender_id? } | Fire when a message in a channel matches a regex/string |
mention | { channel_ids?: string[] } | Fire when @mentioned (workspace-wide unless channels narrowed) |
channel_event | { channel_id, event_type: "reaction_added"|"member_joined"... } | Fire on a channel-level event |
webhook | {} (URL + signing secret minted after save) | External system POSTs a payload |
Cron grammar (5-field standard): minute hour dom mon dow
minute 0–59, hour 0–23, dom 1–31 or *, mon 1–12 or *, dow 0–6 or * (Sun=0)1-5), steps (*/15), ranges (9-17) all worktz explicitly. Default to workspace timezone; fall back to "UTC" if unknown."0 9 * * 1-5" = 9am weekdays · "*/15 * * * *" = every 15 min · "0 */4 * * *" = top of every 4th hourUse these EXACT tool names (NOT CLI commands — runtime tool names):
tool | args shape | Output for chaining |
|---|---|---|
send_message | { channel_id, content } | (none — terminal) |
send_dm | { user_id, content } | (none — terminal) |
sql_query | { connection: "<connection_name>", query: "SELECT ..." } | {{stepN.rows}} (array) |
http_request | { connection?: "<name>", method: "GET"|"POST"|..., url, headers?, body? } | {{stepN.body.PATH}} — PROBE the endpoint with curl first to verify PATH actually exists in the response (Step 3.5) |
run_skill | { skill_id, args } | {{stepN.output}} |
For third-party services (Linear, Gmail, Notion, HubSpot, etc.), use http_request with the OAuth connection that ano integrations connect <app> produces. A dedicated pipedream_run action is planned but not yet engine-callable.
Template variables: earlier-step outputs interpolated as {{stepN.PATH}}:
{{step1.rows.length}} — count of SQL rows{{step1.rows.0.name}} — first row's name column{{step2.body.user.email}} — nested HTTP response fieldstepN is 1-indexed in the order they appear in actions[]. Webhook payloads reference as {{step0.body.*}}.
Sender attribution (sender_kind at plan top-level):
bot (default) — generic Ano bot avatarcoworker — pair with coworker_id; messages appear from that coworkerhuman — posts as the human owner; safety lint warns (channel members may think the human typed it)bot_avatar is an optional single emoji (e.g. "📊").
If the plan contains an http_request whose output you reference via {{stepN.body…}}, you MUST probe the endpoint and verify the field path. Do NOT infer the response shape from the URL.
# 1. Probe the real endpoint
curl -s 'https://api.example.com/v1/thing?param=x' | head -c 1000
# 2. Confirm the path lands at a real value
# e.g. body.current.temperature_2m → 11.8 ✓
# body.data.temp → undefined ✗
Bake the verified path into the template. If the body is wrapped or paginated, follow the wrapper.
After saving, verify rendering with a real test fire (NOT just dry-run — dry-run doesn't exercise template substitution):
ano automation run <slug-or-id> --no-dry-run --agent
ano messages read --channel <dm-channel-id> --limit 1 --agent
# Check the delivered message contains real values, not empty placeholders
# (e.g. "Temp: °C" means the path didn't resolve — engine silently empties).
If you skip this and the template path is wrong, runs report success but recipients receive empty placeholders. The engine does NOT fail loudly on missing template paths.
Same rule applies when editing actions via ano automation update --actions ...: probe before, verify after.
| Rule | What to do |
|---|---|
SELECT * going to a public channel | Add LIMIT N (typically 50). Unbounded SELECT * → public = channel flood. |
| Sensitive columns in SQL | Avoid password, secret, token, api_key, salt, private_key, credit_card, ssn. Mask at query time or refuse. |
Posting as human | Default to bot unless user explicitly asks. If human, warn the user channel members will see it as their voice. |
| Public-channel destination | Mention it to the user — they may want a private channel instead. |
| Guest-visible channel | If has_guests=true on destination, flag it — guests see the output. |
| Connection name not found | Still emit the action; warn the user the connection chip will be phantom until they wire it up. |
"for 5 weeks" / "20 times" / "until end of Q2" / "just once" → set caps. Engine auto-disables on cap reached; user can extend.
| Field | Type | In plan JSON | On submit flag |
|---|---|---|---|
max_runs | positive int | "max_runs": 20 | --max-runs 20 |
expires_at | epoch ms (UTC) | "expires_at": 1717200000000 | --expires-in "5 weeks" OR --expires-at 2026-06-01 |
Phrasing → field:
--expires-in / --expires-atmax_runs--expires-in "1 month"Surface in plain-English recap (Step 5):
"Got it: every weekday at 09:00 Stockholm time, post yesterday's signups to #growth, for the next 5 weeks (auto-disables on {date}). Confirm?"
On cap reached: engine sets enabled=false, last_error="cap reached: <max_runs|expired>". Extend with ano automation update <slug> --max-runs 100 --enabled true.
Run this in chat BEFORE any Ano calls. Goal: walk away with every plan field filled in.
send_message to an Ano channel.) For SQL/HTTP steps, ask which connection. For {{stepN...}} references, build them yourself — don't ask the user to write template syntax.ano automation create-compiled --file - --agent (with --max-runs / --expires-in / --expires-at if set).When the automation needs a third-party service the user hasn't connected yet (any service in Pipedream's catalog — Linear, GitHub, Gmail, Notion, HubSpot, PostHog, Slack, Salesforce, etc.), ask them to authorize BEFORE submitting, otherwise the action fails at fire time with missing-connection.
ano integrations connect posthog --agent
CLI returns a Pipedream OAuth URL. Surface as clickable link (styled output wraps in OSC 8; --agent envelope has auth_url). After user finishes OAuth, Pipedream calls back to Ano and the connection persists. The expected_connection_name field shows the deterministic persisted name (pipedream:<app>:<userId>) — useful for polling /api/connections?workspace_id=… to detect completion.
When composing the plan, reference the connection by name in sql_query / http_request actions — the engine looks the credential up by that name at fire time.
⚠ Engine-callable
pipedream_runis NOT live yet. Connection persists, but no dedicated tool wired in. For now, usehttp_requestagainst the third-party API directly with the connection's OAuth token. Dedicated action lands in a follow-up.
ano automation webhook-setup <slug-or-id> returns URL + secret but the token starts in stub mode — incoming events recorded for inspection but actions DON'T fire. Intentional for the desktop UI's recompile-on-real-payload flow. When you (Claude Code) build a webhook automation end-to-end, you almost always want it live immediately.
# 1. Mint the URL + secret
ano automation webhook-setup quiet-otter-42 --agent
# 2. Activate so inbound POSTs fire actions
ano automation activate quiet-otter-42 --agent
The mint response's next_step field contains the activate hint — surface to the user so they don't lose silent webhook fires.
User: "Every weekday at 9am Stockholm time, post the count of new signups from yesterday to #growth."
{
"trigger_type": "schedule",
"trigger_config": { "cron": "0 9 * * 1-5", "tz": "Europe/Stockholm" },
"actions": [
{
"tool": "sql_query",
"args": {
"connection": "neon-prod",
"query": "SELECT COUNT(*) AS n FROM users WHERE created_at::date = CURRENT_DATE - 1 LIMIT 1"
}
},
{
"tool": "send_message",
"args": {
"channel_id": "ch_abc123",
"content": "📊 New signups yesterday: {{step1.rows.0.n}}"
}
}
],
"name": "Daily signup count → #growth",
"sender_kind": "bot",
"bot_avatar": "📊"
}
User: "When @mentioned in #support, query our HelpScout API and DM me the open ticket count."
{
"trigger_type": "mention",
"trigger_config": { "channel_ids": ["ch_support_xyz"] },
"actions": [
{
"tool": "http_request",
"args": {
"connection": "helpscout",
"method": "GET",
"url": "https://api.helpscout.net/v2/conversations?status=active"
}
},
{
"tool": "send_dm",
"args": {
"user_id": "u_user_789",
"content": "Open tickets: {{step1.body._embedded.conversations.length}}"
}
}
],
"name": "Mention in #support → DM ticket count",
"sender_kind": "bot"
}
User: "When Stripe webhooks us a charge.failed event, post the customer email to #billing-alerts."
{
"trigger_type": "webhook",
"trigger_config": {},
"actions": [
{
"tool": "send_message",
"args": {
"channel_id": "ch_billing_alerts",
"content": "❌ Charge failed for {{step0.body.data.object.receipt_email}} — amount {{step0.body.data.object.amount}}"
}
}
],
"name": "Stripe charge.failed → #billing-alerts",
"sender_kind": "bot",
"bot_avatar": "❌"
}
(Webhooks reference inbound payload as {{step0.body.*}}.)
User: "DM Leo every hour during work hours with the weather in Stockholm. Fetch from open-meteo.com."
Step 1 — Resolve "Leo" to user_id (one stateless probe):
ano user_get_by_name "Leo" --agent
# → { user_id: "user_01KGDB7J4R8VEEJ2FNCV0AMPDR", display_name: "Leo Nilsson", … }
Ambiguous name → fall back to ano users list --agent and pick. Don't ask the user for the ID.
Step 2 — Compose offline:
{
"trigger_type": "schedule",
"trigger_config": { "cron": "0 9-17 * * 1-5", "tz": "Europe/Stockholm" },
"actions": [
{
"tool": "http_request",
"args": {
"method": "GET",
"url": "https://api.open-meteo.com/v1/forecast?latitude=59.3293&longitude=18.0686¤t=temperature_2m,weather_code,wind_speed_10m"
}
},
{
"tool": "send_dm",
"args": {
"user_id": "user_01KGDB7J4R8VEEJ2FNCV0AMPDR",
"content": "Stockholm weather: {{step1.body.current.temperature_2m}}°C, wind {{step1.body.current.wind_speed_10m}} km/h"
}
}
],
"name": "Hourly Stockholm weather → Leo",
"sender_kind": "bot",
"bot_avatar": "☀️"
}
0 9-17 * * 1-5 = at minute 0 of hours 9–17 on Mon–Fri, interpreted in Europe/Stockholm.
Step 3 — Validate offline:
echo '{...plan...}' | ano automation validate --file - --agent
Step 4 — Plain-English recap, confirm with user. Do NOT show JSON.
"Every weekday from 9am to 5pm Stockholm time, on the hour, I'll fetch the current Stockholm weather from open-meteo and DM Leo a one-line summary (temperature + wind). Confirm?"
Step 5 — On confirm, submit once:
cat /tmp/plan.json | ano automation create-compiled --file - --agent
# → { id: "auto_…", next_fire_at: "2026-05-04T08:00:00Z", … }
Step 6 — Report back:
"Created. First fire in 47 minutes."
Total elapsed: ~2 seconds. No nested-Claude-Code session. No state loss between Bash invocations. This is the flow for any from-chat automation request — never ano new automation.
trigger_type is one of the five literalstrigger_config matches the shape for that trigger typeschedule: cron parses as 5 fields; tz is setactions has ≥1 entrytool is one of the five literalschannel_id / user_id / connection resolves to something the user named in the interview{{stepN.PATH}} reference points at a step that produces that outputhttp_request template ref: I have curled the endpoint and confirmed PATH resolves to a real value (Step 3.5). Skipping = empty placeholders in production.name is ≤80 chars, human-readablesender_kind is set; coworker_id is present iff sender_kind="coworker"SELECT * to a public channel without LIMIT# Verify
ano automation list --agent
# Test (dry-run)
ano automation run auto_abc --agent
# Fire for real once
ano automation run auto_abc --no-dry-run --agent
User enables it in the Ano UI — deliberate human gate so an automation never silently starts firing.
Guides creation, editing, and verification of skills for AI coding agents using test-driven development with subagent scenarios. Use when authoring or debugging skills.
npx claudepluginhub ano-chat/ano-skills --plugin ano-skills