{"slug":"automations","title":"automations","summary":"Writing Cerb automations in KATA -- naming, command shape, the correctness rules that bite, and least-privilege policies. Load it before writing or editing any automation script or policy.","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-10-16T16:27:56.818085Z","repo":{"url":"https://github.com/jstanden/cerb","stars":84,"forks":38,"license":null,"updatedAt":"2026-10-16T15:09:59Z"},"bodyHtml":"<hr>\n<h2>name: automations\ndescription: Writing Cerb automations in KATA -- naming, command shape, the correctness rules that bite, and least-privilege policies. Load it before writing or editing any automation script or policy.</h2>\n<h1>Cerb automations (KATA)</h1>\n<p>An automation is a KATA script bound to a trigger, plus a policy that grants the script its privileges. The two move together: a script that gains a privileged command stops working until the policy allows it.</p>\n<h2>Knowledge — use the mounted documentation, don't guess</h2>\n<p><code>cerb-docs</code> contains the Cerb documentation for KATA syntax and annotations, automation commands, triggers, events, policies, record types, search queries, toolbars, workflows, and guides. Consult it before writing anything you're unsure of — especially <code>@cerb-docs/references/docs/automations.md</code>, <code>@cerb-docs/references/docs/kata.md</code>, and <code>@cerb-docs/references/docs/records/types.md</code>.</p>\n<p>Search <code>cerb-docs</code> for the exact command, trigger, event, field, or filter you need rather than inventing syntax. Cite what you relied on when it clarifies your change.</p>\n<p><strong>Read a command's own page before you write that command -- every time, including commands you are confident you already know.</strong> Recall is reliable about which command to reach for and unreliable about what to type inside it: parameter names, which inputs are required, what the output dictionary contains. A wrong input name is accepted when the automation is saved and fails when it runs, so there is nothing to catch it in between. One <code>read</code> is cheaper than that.</p>\n<h2>Naming automations</h2>\n<ul>\n<li>An automation's <code>name</code> is an IDENTIFIER, not a title. It's how the automation is addressed everywhere else — <code>uri: cerb:automation:example.math.sum</code> in function:, toolbars, timers, and data queries. Human-friendly wording belongs in <code>description:</code>, not in the name.</li>\n<li>Allowed characters: lowercase letters (a-z), numbers (0-9), underscores (<code>_</code>), and dots (<code>.</code>) — the same set Cerb requires for plugin and extension IDs (@cerb-docs/references/docs/plugins.md). No spaces, dashes, or slashes.</li>\n<li>Dots delimit a namespace hierarchy, broadest to most specific. The conventional shape is <code>&lt;namespace&gt;.&lt;area&gt;.&lt;thing&gt;</code> -- e.g. <code>example.math.sum</code>, <code>example.translate.function</code>, <code>example.automationTimer.createDailyTasks</code>, <code>wgm.example.openai</code>.</li>\n<li>The FIRST segment is a namespace unique to the author/org — conventionally based on a domain you own (wgm, acme) so names stay globally unique. Reserve <code>cerb.</code> for Cerb itself and <code>example.</code> for documentation samples.</li>\n<li>Middle segments group related automations: by trigger (automationTimer, interactionWorker), by feature area (mail, billing), or by calling subsystem. Keep grouping consistent — prefix search (<code>name:example.mail.*</code>) is how people find them in worklists.</li>\n<li>The LAST segment names the specific behavior; camelCase is conventional there (createDailyTasks, openai). Prefer a verb phrase for automations that act (sendWelcomeEmail) and a noun for ones that fetch (openTicketCount).</li>\n<li>Match the namespace and style already used in this install rather than introducing a new scheme; name a new function in the same namespace as its caller. Renaming breaks every <code>cerb:automation:</code> URI pointing at it, so call out affected call sites when you propose a rename.</li>\n<li>Don't confuse this with command aliases inside a script (<code>set/a:</code>, <code>await/step:</code>), which allow only letters, numbers, and underscores — no dots.</li>\n</ul>\n<h2>Automation correctness rules (common pitfalls — honor them)</h2>\n<p>These are the automation-specific ones. The language itself — indentation, quoting, comments, annotations, unique sibling keys — is the <code>kata</code> skill, and its indentation rule bites here constantly: a bare <code>key:</code> with indented lines under it is an OBJECT, so any multi-line or prose value needs <code>@text:</code> on its key.</p>\n<ul>\n<li>Every automation starts at start:. The working dictionary begins as a copy of inputs and is mutated as it runs.</li>\n<li>await: resumes AFTER the await, not from start:. Commands above the await do not re-run (their results persist in the continuation). A per-item/paging UI must put the await INSIDE a while: loop, or the next resume falls off the end and returns nothing. Build source lists before the loop.</li>\n<li>decision:/outcome: use first-match semantics; an outcome without if@bool: is the default. Write conditions as if@bool: {{...}}</li>\n<li>repeat: requires <code>each:</code> to resolve to an ARRAY, and a bare <code>each: {{...}}</code> does not -- a placeholder renders to TEXT. Annotate the key: <code>each@key: results.data</code> when the array is already in a placeholder (<code>@key</code> reads the dictionary value and keeps its type, so there's no round-trip through text), otherwise <code>@csv</code> / <code>@json</code> / <code>@list</code>, or native KATA children. A non-array -- or absent -- <code>each:</code> is NOT an error: <code>repeat:</code> runs ZERO iterations and moves on, so the symptom is a loop body that silently never executes.</li>\n<li>while: takes <code>if:</code>, and the same bare-key-renders-TEXT problem inverts here. Any text that isn't <code>false</code>/<code>0</code>/<code>no</code>/<code>n</code>/<code>off</code>/empty is TRUE, so <code>if:</code> fed a rendered string is true forever and the loop only stops at the policy time limit. <code>if:</code> and <code>if@bool:</code> are equivalent (both end at the same toBool), so what matters is that the expression renders to <code>1</code>/empty -- write a comparison, not a value. Unlike <code>repeat:</code>, a missing <code>if:</code> IS a hard error.</li>\n<li>Never stack @raw inside an already-raw block — <code>script@raw:</code>, <code>event_kata@raw:</code>, and <code>policy_kata@raw:</code> are where this comes up. Use the normal annotations inside them (<code>if@bool:</code>, <code>set:</code>); <code>if@raw,bool: {{...}}</code> makes the condition never match.</li>\n<li>For Twig arrays in set:, match the annotation to the rendered text: pipe through <code>|json_encode</code> for @json; prefer native KATA (0:, 1:, …) for static arrays of objects.</li>\n<li>Choose the right output field for the trigger's return keys, and match inputs/ placeholders to the trigger (e.g. interaction.worker exposes worker_* directly).</li>\n<li>icon: values must come from the Cerb icon set — invented names silently render nothing. When unsure, reuse an icon already present in the automation rather than guessing a new name; the <code>icons</code> skill covers the set and its naming conventions.</li>\n</ul>\n<h2>Command shape — action commands take <code>output:</code> and <code>inputs:</code></h2>\n<p>Action commands do NOT take their parameters directly. Each takes an <code>output:</code> (the placeholder that receives the result — name one whenever you need the result) plus an <code>inputs:</code> block holding every documented parameter, with the handlers <code>on_simulate:</code>/<code>on_success:</code>/<code>on_error:</code> as siblings of those keys:</p>\n<pre><code>  start:\n    record.create/newTask:\n      output: new_task\n      inputs:\n        record_type: task\n        fields:\n          title: This is a new task\n          status: open\n      on_success:\n        return:\n          task_id: {{new_task.id}}\n</code></pre>\n<p>Wrong — parameters hung directly off the command. <code>record_type:</code>/<code>fields:</code> are not recognized there, so the command gets no inputs and fails:</p>\n<pre><code>  record.create:\n    record_type: task\n    fields:\n      title: This is a new task\n</code></pre>\n<p>This shape applies to record.<em>/records.update:, http.request:, data.query:, function:, storage.</em>, file.<em>, kata.parse:, llm.chat:/llm.agent:/llm.embed:/llm.router:, queue.</em>, metric.increment:, email.parse:, encrypt.pgp:/decrypt.pgp:, api.command:, and var.set:/var.push:/var.unset:/var.expand:.</p>\n<p>The exceptions are the state and flow keywords, which take their parameters directly and have no output:/inputs:  — return:, error:, await:, tool.return:, set:, decision:/outcome:, repeat:, while:, and log:/log.warn:/log.error:/log.alert:.</p>\n<p><code>log:</code> is the sharpest exception: unlike every other action it takes a SINGLE STRING as its value and nothing else -- no <code>inputs:</code>, no <code>output:</code>, no <code>on_success:</code>/<code>on_error:</code>.</p>\n<pre><code>  log: Ticket {{ticket_id}} was routed to {{group_name}}\n</code></pre>\n<p>Give it a block instead and the value is empty, which <code>log:</code> treats as nothing to log -- it writes no entry and reports no error. The four severities (<code>log:</code> debug, <code>log.warn:</code>, <code>log.error:</code>, <code>log.alert:</code>) all take the same bare string.</p>\n<p>Results read off the output placeholder after the call ({{new_task.id}}), and an error reads off that same placeholder inside on_error: ({{new_task.error}}) — no separate output key is needed for errors. Look up a command's exact input table on its own page (below) instead of guessing parameter names.</p>\n<h2>Available Commands (quick reference)</h2>\n<p>This list is an INDEX, not syntax. It says a command exists and what it is for; it does not tell you what to type. Pick the command from here, then <code>read</code> its page and write from that.</p>\n<p>Each command has its own page at <code>@cerb-docs/references/docs/automations/commands/&lt;command&gt;.md</code> -- <code>read</code> it directly for full syntax, parameters, and examples. Go straight there rather than searching: a name like <code>set</code> or <code>return</code> appears in every corner of the docs, and the search costs turns to land where one <code>read</code> would have. Five have no page of their own -- <code>start</code>, <code>tool.return</code>, and the <code>log.warn</code>/<code>log.error</code>/<code>log.alert</code> variants (covered by <code>log.md</code>); for those, and for the overview, read <code>@cerb-docs/references/docs/automations.md</code>.</p>\n<p><strong>State:</strong> <code>return:</code> (success), <code>error:</code> (fail), <code>await:</code> (pause/continuation), <code>tool.return:</code> (answer a tool call -- only valid inside an <code>llm.agent:</code> <code>on_tool:</code> branch)</p>\n<p><strong>Flow:</strong> <code>decision:</code>/<code>outcome:</code> (first-match branching), <code>repeat:</code> (iterate array), <code>while:</code> (loop)</p>\n<p><strong>Variables:</strong> <code>set:</code>, <code>var.set:</code> (key path), <code>var.push:</code> (append), <code>var.unset:</code> (remove), <code>var.expand:</code> (lazy-load)</p>\n<p><strong>Records:</strong> <code>record.create:</code>, <code>record.get:</code>, <code>record.search:</code>, <code>record.update:</code>, <code>record.upsert:</code>, <code>record.delete:</code>, <code>records.update:</code> (one call updating many records by id -- reach for it instead of wrapping <code>record.update:</code> in a <code>repeat:</code>)</p>\n<p><strong>HTTP:</strong> <code>http.request:</code> (GET/POST/PUT/PATCH/DELETE, streaming, auth)</p>\n<p><strong>Data:</strong> <code>data.query:</code> (worklist queries, custom formats)</p>\n<p><strong>Functions:</strong> <code>function:</code> (call another automation)</p>\n<p><strong>Storage:</strong> <code>storage.get:</code>, <code>storage.set:</code>, <code>storage.delete:</code> (key/value, optional TTL)</p>\n<p><strong>Files:</strong> <code>file.read:</code> (attachments/resources), <code>file.write:</code> (automation resources)</p>\n<p><strong>KATA:</strong> <code>kata.parse:</code> (parse KATA with placeholders)</p>\n<p><strong>LLM:</strong> <code>llm.chat:</code> (single-turn), <code>llm.agent:</code> (conversational + tools), <code>llm.embed:</code> (vectors), <code>llm.router:</code> (resolve a model from requirements instead of naming one)</p>\n<p><strong>Queues:</strong> <code>queue.push:</code>, <code>queue.pop:</code> (message queues)</p>\n<p><strong>Metrics:</strong> <code>metric.increment:</code> (time-series samples)</p>\n<p><strong>Email:</strong> <code>email.parse:</code> (MIME → ticket)</p>\n<p><strong>Encryption:</strong> <code>encrypt.pgp:</code>, <code>decrypt.pgp:</code></p>\n<p><strong>API:</strong> <code>api.command:</code> (internal Cerb commands)</p>\n<p><strong>Logging:</strong> <code>log:</code>, <code>log.warn:</code>, <code>log.error:</code>, <code>log.alert:</code></p>\n<p><strong>Simulation:</strong> <code>simulate.success:</code>, <code>simulate.error:</code> (mock outputs in <code>on_simulate:</code>)</p>\n<h2>Policies (principle of least privilege)</h2>\n<ul>\n<li><strong>A policy denies everything by default.</strong> A command with no rule that matches it is denied, so a policy is only ever a list of what you are allowing. Do NOT write an <code>all:</code> / <code>deny: yes</code> catch-all at the end -- it grants nothing, denies nothing already permitted, and reads as though the rules above it were open-ended.</li>\n<li>Allowlist every policy-gated command the script uses. <strong>Only action commands are gated.</strong> The state and flow keywords -- <code>return:</code>, <code>error:</code>, <code>await:</code>, <code>tool.return:</code>, <code>set:</code>, <code>decision:</code>/<code>outcome:</code>, <code>repeat:</code>, <code>while:</code>, and <code>log:</code>/<code>log.warn:</code>/<code>log.error:</code>/<code>log.alert:</code> -- run with no rule at all, and so do <code>var.set:</code>/<code>var.push:</code>/<code>var.unset:</code>/<code>var.expand:</code> and <code>kata.parse:</code> despite taking <code>output:</code>/<code>inputs:</code>. Entries for those are noise; leave them out.</li>\n<li><code>records.update:</code> is gated separately from <code>record.update:</code> -- allowing one does not allow the other.</li>\n<li>Each command entry holds only allow/deny children (optionally /named).</li>\n<li>Guard by record type or URL with the deny-first idiom, then <code>allow@bool: yes</code>. Do NOT try a positive allow/...@bool with a ternary on inputs.record_type — it does not work.</li>\n<li>Add callers: only when restricting which toolbars may invoke an interaction.</li>\n</ul>\n<p>The deny-first idiom:</p>\n<pre><code>deny/type@bool: {{inputs.record_type is not record type ('task')}}\nallow@bool: yes\n</code></pre>\n<h2>Style</h2>\n<ul>\n<li>Keep KATA idiomatic — see the <code>kata</code> skill for the language's own conventions.</li>\n<li>Flag bugs or risky patterns you notice even when unasked.</li>\n</ul>\n<h2>Related skills</h2>\n<ul>\n<li><code>kata</code> — the language this is written in: the indentation rule, quoting, comments, annotations.</li>\n<li><code>scripting</code> — the Twig inside a <code>{{...}}</code> placeholder or a <code>script@raw:</code> value.</li>\n<li><code>search-queries</code> — writing a <code>record_query:</code> for <code>record.search:</code> / <code>data.query:</code>.</li>\n<li><code>records</code> — confirming a record type's alias, its writable fields, and its search filters.</li>\n<li><code>data-queries</code> — the <code>data.query:</code> command's own query language.</li>\n</ul>\n","files":[{"path":"SKILL.md","sizeBytes":12765,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-10-16T16:28:13.393249Z","sha256":"01FBAA1841C1E22CEEEA3906710387BB9FC72724672F4ABCBE31AA62563644C8","sizeBytes":5552},"review":null,"source":{"repositoryUrl":"https://github.com/jstanden/cerb","path":"features/cerberusweb.core/assets/agent_filesystems/cerb-agents/skills/automations","license":null,"commit":"cf28b62ee0f67ff993645931568a4e4bbad074f0","subtreeSha":"455C0752A20272FB81F612E55B94EDAF8FD00BD017C2FAE1421BDE5D77FD70AE","lastSyncedAt":"2026-10-16T16:27:56.786317Z"},"reviewedAt":"2026-10-16T16:28:23.539733Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/jstanden/cerb/tree/12.0.x/features/cerberusweb.core/assets/agent_filesystems/cerb-agents/skills/automations"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install jstanden-cerb@llmmart"},{"target":"git","command":"git clone https://github.com/jstanden/cerb.git"}]}