# Statements

> Send, wait, branch, split, hold out, repeat, set, add to list, exit — the whole executable surface.

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

Nine statements. That is the entire executable surface of the language.

## send

The point of the whole exercise. Three forms, mutually exclusive.

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

```sendflow
send "welcome"                              // inherit the template's default topic
send "welcome" via topic "onboarding"       // explicit topic override
send "welcome" transactional                // topic-exempt lifecycle mail
```

The **template** is always a quoted string, and it has to exist.

The **topic** is the consent grouping the message is sent under. A contact who
has opted out of that topic does not receive it. `via topic` overrides whatever
the template's own default topic is.

**`transactional`** marks mail that is exempt from topic consent — a receipt, a
password reset, a legally-required notice. It is not a way to bypass someone's
preferences for marketing, and treating it as one is how a sending domain gets
blocked.

A topic is a **quoted string**, always. Writing it bare is the single most common
mistake in a `send`:

**Rejected — the parser refuses this:**

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

  send "welcome" via topic onboarding
}
```

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

Quoting them means a topic can be called anything at all — including a reserved
word. `via topic "timeout"` is legal.

## wait

Three forms, plus a fourth that waits on a *condition* rather than a clock.

**Wait a fixed duration.**

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

```sendflow
wait 2d
```

**Wait until a date**, absolute or from the contact's own attribute:

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

```sendflow
wait until 2026-12-01
wait until attr.renewal_date
```

The attribute must be a `datetime`.

**Wait for a condition, with a bound.** This is the one that makes drip campaigns
feel alive:

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

```sendflow
wait up to 7d until count(activity.login within 7d) >= 2 {
  timeout: send "need-a-hand" via topic "onboarding"
}
```

Read it as: *wait for up to seven days for them to log in twice. If they do,
carry straight on. If seven days pass and they have not, send the fallback, then
carry on.*

The `timeout:` arm is **optional**. Without it, the flow simply continues when
the bound expires:

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

```sendflow
wait up to 3d until exists(open)
```

The timeout arm can hold several statements, not just one:

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

```sendflow
wait up to 7d until exists(activity.order) {
  timeout:
    send "last-call" via topic "marketing"
    wait 1d
    add to list "lapsed"
}
```

## if / else if / else

Branching. Both arms are always brace-delimited, so there is no dangling-else
ambiguity to reason about.

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

```sendflow
if attr.plan = "pro" {
  send "pro-tips" via topic "onboarding"
} else if attr.plan = "team" {
  send "team-tips" via topic "onboarding"
} else {
  send "free-tips" via topic "onboarding"
}
```

Arms **rejoin**. After the block ends, control continues at the next statement —
a branch is a fork in the path, not an ending. To actually end the flow inside an
arm, say `exit`.

## split

A weighted random fork — the A/B test.

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

```sendflow
split {
  30%: {
    send "a" via topic "marketing"
  }
  70%: {
    send "b" via topic "marketing"
  }
}
```

The weights must sum to **exactly 100**. Anything else is an error, caught before
the campaign ever runs:

**Rejected — the parser refuses this:**

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

  split {
    30%: { send "a" via topic "marketing" }
    30%: { send "b" via topic "marketing" }
  }
}
```

Like `if`, split arms rejoin afterwards. A split with a single arm is a warning —
you almost certainly meant `hold out`.

## hold out

A control group. The named percentage **leaves the workflow here**; everyone else
carries on.

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

```sendflow
hold out 10%
```

That is the difference from `split`: a split sends everyone down *some* path,
while a holdout removes people from the campaign entirely so you can measure what
would have happened without it. Ten percent of contacts get nothing, on purpose,
and the lift you measure against them is the only honest number you will get.

Must be between 1% and 99%.

## repeat

The only loop, and it is always bounded.

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

```sendflow
repeat up to 2 every 24h until exists(activity.order) {
  send "cart-nudge" via topic "marketing"
}
```

*Up to twice, a day apart, stopping early if they order.*

- `up to N` is **mandatory**. A `repeat` with no bound is a parse error. There is
  no way to write an unbounded loop in SendFlow, which is why a workflow cannot
  turn into a mail bomb.
- `every <duration>` is the spacing between iterations.
- `until <predicate>` is optional, and is checked before each iteration.

## set

Write to a contact attribute.

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

```sendflow
set attr.nudged = true
set attr.tier = "vip"
set attr.score = 10
set attr.last_contacted = now
```

The value's type must match the attribute's — `set attr.score = "high"` is an
error — and an enum attribute may only be set to an allowed value.

## add to list

Add the contact to a named list.

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

```sendflow
add to list "engaged"
```

## exit

Leave the workflow, immediately and terminally.

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

```sendflow
exit
```

Note the difference from the `exit "name" when ...` **setting**: that one declares
a goal checked everywhere, while this one is a statement that ends the flow right
where it stands. The bare `exit` takes no name.

Anything after it in the same block is unreachable, and you will be told so:

> `warning: unreachable: this statement follows an exit`

## Everything at once

```sendflow kitchen-sink.flow
workflow "Cart abandonment" v1 {
  enter on activity.cart_updated where not exists(activity.order within 1h)
  exit "purchased" when exists(activity.order)
  reentry per occurrence
  send window 9am-8pm in contact timezone

  wait 1h
  hold out 10%

  if attr.plan = "pro" {
    send "cart-reminder" via topic "billing"
  } else {
    send "cart-reminder" via topic "marketing"
  }

  repeat up to 2 every 24h until exists(activity.order) {
    send "cart-nudge" via topic "marketing"
  }

  set attr.nudged = true
  add to list "engaged"
}
```
