# Formatting and style

> One canonical layout, no options — what sendflow-fmt does to your file, and what it preserves.

Source: https://www.sendlang.com/docs/sendflow/formatting
Section: SendFlow

SendFlow ships a formatter with **no configuration**. There is one canonical
layout and no way to argue about it, for the same reason Go has `gofmt`: the
argument is worth less than the time it costs.

```
$ sendflow-fmt -w flows/          # rewrite in place
$ sendflow-fmt -l flows/          # list unformatted files, exit 1 if any — the CI gate
$ sendflow-fmt -d winback.flow    # show what would change
```

## What it guarantees

Formatting is a **fixed point**, not byte preservation: `fmt(fmt(x)) == fmt(x)`.
Run it twice and the second run changes nothing. That is the contract, and it is
what makes `-l` safe to put in CI.

What it does *not* guarantee is that your file comes back byte-for-byte. Your
hand-tuned indentation will be replaced. That is the trade.

## The rules

- **Two-space indentation**, one level per nesting depth.
- **Single spaces** around every keyword and operator. All hand-whitespace is
  collapsed: `attr.plan="pro"  and  attr.score>=5` becomes
  `attr.plan = "pro" and attr.score >= 5`.
- **Embedded predicates print on one line**, always. Parentheses you wrote are
  reproduced exactly — never invented, never dropped.
- **Blocks open on the head line**: `if ... {`, never a brace on its own line.
  `else` splices onto the closing brace: `} else if ... {`.
- **Exactly one blank line between the settings and the statements.** It is
  inserted whether or not you wrote one.
- **Clock minutes are dropped when zero**: `9:00am` becomes `9am`.
- **A short `timeout:` arm inlines.** If the whole arm is one simple statement it
  prints on one line; otherwise it breaks and indents.

Your **blank-line paragraphing is preserved** everywhere else. If you grouped
three sends together and left a gap before the next section, the gap stays — the
formatter normalises syntax, not your sense of rhythm.

## Comments

`//` to the end of the line, and they survive formatting.

```sendflow commented.flow
// A file header, kept above the workflow line.
workflow "Commented" v1 {
  // A leading comment on the trigger.
  enter on segment "trial-started"

  // A paragraph of its own,
  // continuing here.
  send "welcome" via topic "onboarding" // and a trailing note

  wait 3d
  if attr.plan = "pro" {
    // Inside the branch.
    exit
  }
}
```

Comments are attached to the thing below them (or beside them, if they are on the
same line), which means they travel with that statement — including when a visual
editor moves it. They are not a position-keyed side-table that goes stale the
moment anything shifts.

**One known limitation.** A comment on a line where nothing else starts — between
an `else` and its `{`, or trailing the `workflow` header line — has nothing to
attach to, and relocates to the next thing that does. The result is stable and
re-formats identically; it is just not where you put it.

## Style, beyond what the formatter enforces

The formatter will not do these for you, and they are worth doing.

**Name the goal, do not check for it.** An `exit ... when` is evaluated before
every step. Reach for it before you reach for a defensive `if`.

**Syntax form, not a complete program:**

```sendflow
// Prefer this
exit "purchased" when exists(activity.order)

// over guarding every send
if not exists(activity.order) { send "nudge" via topic "marketing" }
```

**Extract a complicated predicate into a segment.** `if in segment
"high-value-at-risk"` is testable on its own, reusable across campaigns, and one
place to change.

**Say `not suppressed` in your trigger filter.** It costs six characters.

**Bound your backfill.** If you want `enroll existing`, add a `since` window
unless you have genuinely thought about mailing every contact who has ever
matched.

**Put the holdout early.** `hold out 10%` right after the trigger measures the
whole campaign. Halfway down, it only measures the tail.
