# Coding agents

> SendLang is built so a coding agent can author and refactor your campaigns — a published grammar, a type checker to iterate against, and a diff a human approves before anything sends.

Source: https://www.sendlang.com/docs/agents
Section: Introduction

A drag-and-drop canvas is a dead end for a coding agent. It cannot see the
campaign, cannot change it, and cannot tell you what it changed. Making these
languages *languages* is what puts lifecycle email inside the loop an agent
already works in: read the files, write the files, run the checker, open a pull
request.

This is not an afterthought of the design. It is one of the reasons the design
exists.

## What an agent needs, and where it is

An agent needs a spec it can load, a checker it can run, and errors it can read.
All three are published.

| What | Where |
|---|---|
| The whole reference, in one file | [`/llms-full.txt`](/llms-full.txt) |
| An index of every page, as Markdown | [`/llms.txt`](/llms.txt) |
| Any page, as Markdown | Append `.md` — [`/docs/sendql/events.md`](/docs/sendql/events.md) |
| The normative grammar | [EBNF](/docs/reference/grammar), generated from the parser itself |
| Every word it may not use as a name | [Reserved words](/docs/reference/reserved-words) |
| Every error it can be told | [The diagnostics catalogue](/docs/reference/errors) |

The grammar and the reserved-word list are *emitted by the parsers*, not
transcribed by hand, so the spec an agent reads cannot drift from the code that
judges its output.

The shortest useful thing you can do: put `https://www.sendlang.com/llms-full.txt`
in the agent's context. That is the entire language — both of them — in one
fetch.

## The loop

Give an agent an outcome:

> Three days before a trial ends, email them. If they have not clicked within
> three days, send the upgrade offer.

It writes a file:

```sendflow trial-ending.flow
workflow "Trial ending" v1 {
  enter on 3d before attr.trial_ends_at
  exit "upgraded" when attr.plan != "trial"

  send "trial-ending" via topic "onboarding"
  wait up to 3d until exists(click where template = "trial-ending" within 3d) {
    timeout: send "upgrade-offer" via topic "marketing"
  }
}
```

And then — this is the part a canvas cannot offer — the file is *checked*, before
it is anywhere near a contact.

```
$ sendflow-fmt -l flows/                              # canonical layout, or exit 1
$ sendflow-lint -registry registry.json flows/*.flow
flows/trial-ending.flow: ok
```

`sendflow-lint` parses, resolves every name against your
[registry](/docs/tooling#sendflow-lint), and type-checks the result. Its exit
code is the agent's ground truth: `0` means the workflow is real. Anything else
comes back as a positioned message the agent can act on, which is the whole
trick — **the type checker is the feedback loop.** The agent does not have to be
right first time. It has to be right before the run is clean, and nothing sends
until it is.

Then the diff lands in a pull request, a human reads six lines of text, and
merges. Nobody has to reverse-engineer a canvas to know what changed.

## The registry is the ground truth

An agent's most common failure is confident invention: an attribute that sounds
right and does not exist. The analyzer settles it.

**Rejected — the parser refuses this:**

```sendql
attr.plna = "pro" and count(open within 30d) >= 3
```

> `1:1: error: unknown attribute "plna"`

A grammatically flawless predicate that names something you do not have. The
registry — your attributes, events, activities, topics, templates and lists — is
what turns a plausible guess into a build failure. Give the agent that JSON file
along with the task and it stops guessing altogether.

## The three mistakes an agent will make

They are the same three a person makes, and the parser names the fix in every
case.

**Durations are compact.** Written-out units read naturally and are not the
syntax.

**Rejected — the parser refuses this:**

```sendflow
workflow "Trial ending" v1 {
  enter on segment "trial-started"

  send "trial-ending" via topic "onboarding"
  wait 2 days
}
```

> `5:3: durations are written in the compact form: use 2d, not 2 days (a workflow and a SendQL condition spell a duration the same way now)`

**Topic names are quoted.** A bare word looks like an identifier and is not one.

**Rejected — the parser refuses this:**

```sendflow
workflow "Trial ending" v1 {
  enter on segment "trial-started"

  send "trial-ending" via topic onboarding
}
```

> `4:23: topic names are quoted: use via topic "onboarding", not via topic onboarding`

**Events are not activities.** The eight events are a closed set about email
*you* sent. Anything the contact did in *your* product is an activity, and needs
the qualifier.

**Rejected — the parser refuses this:**

```sendql
exists(order)
```

> `1:8: error: unknown event "order"`

Written `exists(activity.order)`, it is fine. This is the most common error in
the language — see [events and activities](/docs/sendql/events).

There is a fourth trap that is not an error at all: **absence does not compare**.
`attr.plan != "pro"` does not match a contact with no plan, and
`last(open) < now - 90d` excludes everyone who never opened. The analyzer warns,
and an agent should be told to read the warnings too. See
[the absence contract](/docs/sendql/absence).

## Why you can let it

The reason it is safe to hand these files to an agent is not that the agent is
careful. It is that the language will not let either of you write the dangerous
thing.

- **Nothing evaluates.** The parsers parse, analyze and print. There is no
  arbitrary expression evaluation and no way to call out into your code.
- **Every workflow terminates.** There is no `goto` and no unbounded loop — the
  only loop is `repeat up to N every <duration> until <condition>`, bounded twice
  over.
- **Every name is resolved.** A template, topic or list that does not exist is an
  error, not a silent no-op at 3am.
- **You are still the approver.** The output is a diff. CI runs the formatter and
  the linter; a person clicks merge.

The worst thing an agent can hand you is a workflow that is *wrong*, and you will
see it in the diff — in six lines of text, in a pull request, before it has sent
anything.

## Prompting it well

Three things to put in the agent's context, in order of value:

1. **`/llms-full.txt`** — the complete reference. Both languages fit comfortably
   in a modern context window.
2. **Your registry JSON** — so it uses your attribute names, your templates, and
   your topics rather than inventing plausible ones.
3. **The instruction to run the checker.** `sendflow-lint -registry registry.json`
   before it proposes the change, not after you have asked why the campaign never
   sent.

The rest is the loop it already knows.
