Writing an automation in Claritty Studio | Claritty docs

Writing an automation

Your coding agent writes most of this, and the seed hands it the same rules you are reading. What is worth knowing yourself is where the line between a tool and an agent falls, and the four mistakes that pass every check, boot cleanly, and then fail on the first real run.

Five primitives, and one manifest

intelligence.yaml is the source of truth. Everything the automation can do is declared there, and the runtime validates the lot at boot and refuses to start if it does not hold together. That is a feature. Fix the manifest rather than working around it.

PrimitiveIsIs not
integrationa credential this automation needsa piece of code
toola callable action. Deterministic, no model calla decision
agenta prompt that decides which tools to calla place to put business logic
workflowa DAG of steps, each running one agent or one toola script
triggerthe dispatcher that fires one workflow or agentsomething the automation runs itself
schemaVersion: 2
id: overdue-invoice-chaser

integrations: [{ id: gmail, required: true }]

tools:
  - id: app.list_overdue
    handler: backend.tools.list_overdue:run
    output: { invoices: { type: array, required: true } }

agents:
  - id: chaser
    tools: [app.list_overdue, gmail.send]
    promptFile: backend/agents/chaser.md

workflows:
  - id: chase
    steps: [{ id: run, agent: chaser }]

triggers:
  - id: weekday-morning
    type: SCHEDULE
    workflow: chase
    supportedSchedules: [DAILY]
    configFields:
      - { key: time, type: time, required: true, label: "Run at", default: "09:00" }
      - { key: timezone, type: timezone, required: true, label: "Timezone" }

Tools act. Agents decide.

Anything you can state as a rule belongs in a tool: fetching, filtering, formatting, writing a row. Tools are ordinary Python, they take an input and return a dict matching their declared output, and they never call a model. Put the deterministic part there and it is testable, cheap, and does the same thing every run.

An agent is for the judgement a rule cannot express: is this email a real request, do these two issues describe the same bug, which of these results is worth reporting. An agent is a prompt plus a list of tools it may call. Business logic in a prompt is logic you cannot test, and a model call inside a tool is a decision nobody can see.

Tools reach the outside world only through the integration broker, never through an environment variable. A key in your code cannot be rotated, scoped or revoked, and the automation's process is never given one anyway. See Connecting a service.

Four things that only fail when it runs

Every one of these passes the offline gate, boots cleanly, and dies on the first real run. They are listed because each one cost a debugging session.

A fan-out collects into an object, not a list

A forEach step collects into {items: [...]}, so the next step reads ${steps.fanout.output.items}. Writing ${steps.fanout.output} hands the following tool a dict where it declared an array, and the run dies at the last step having already done all the work.

An optional workflow input still binds as null

${input.folder} on a run where nobody supplied folder resolves to null, and a tool field declared type: string rejects null even with required: false. Do not wire an optional workflow input into a tool field. Give the tool its own default, or make the input genuinely required.

A tool the prompt never mentions is never called

Listing a tool in an agent's tools: grants permission. It does not cause a call. If the prompt does not name the tool, the step succeeds, does less than it looks like it did, and reports success. This is the single most common way an automation ends up looking like it works and doing nothing.

A tool id nothing implements fails at call time, not at boot

The integration manifests describe a larger hosted set than this machine can reach. A tool marked as not available locally names something real that Studio cannot broker, and calling it fails mid-run however sensible the id looks. brave-search.search reads correctly and does not exist. The tool is brave-search.web. Inside an automation, catalog/AVAILABLE.md is the only list of what can actually be called, and the connector page here is generated from the same specs.

One more that is not a trap but reads like one: a run whose steps all skipped still reports status: success, because skipping is a strategy rather than a failure. Read the step timeline before taking the engine's verdict as proof the work happened.

The loop while you work

After any change, run the offline gate. It checks manifest semantics and scans for secrets, costs nothing, and catches most of what would otherwise fail at boot:

claritty-seed-verify .

Then run it for real. In Studio both of these are buttons, and the run appears as a step-by-step trace with each model call and what it cost. In the terminal:

node apps/cli/dist/index.js run --native --simulate   # wiring only, nothing spent
node apps/cli/dist/index.js run --native              # for real

Already have an agent written in something else? Do not rewrite it. Map its side effects to tools, its prompts to agents, its entry point to a workflow and its cron to a trigger, then write thin adapters that call the existing functions unchanged. You keep the code and gain a schedule, traces, a credential vault and a cost ledger.