# Conditions

> How SendQL embeds into a workflow — the four places a predicate can go, and what each one means.

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

SendFlow does not have its own condition syntax. Every condition in a workflow is
a [SendQL](/docs/sendql) predicate, parsed by SendQL's grammar and checked by
SendQL's type checker.

Not a string that gets evaluated later. Not a similar-looking dialect. The same
language, embedded — which is why a type error inside a workflow's `if` reports at
the exact line and column of the offending token *in the .flow file*.

## The four places a predicate goes

**`where` — filter the trigger.** Narrows who enters.

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

```sendflow
enter on activity.signup where attr.plan = "free" and not suppressed
```

**`when` — declare a goal.** Checked before every step; the contact leaves the
moment it is true.

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

```sendflow
exit "converted" when attr.plan != "trial"
```

**`if` — branch.** Checked once, at that point in the flow.

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

```sendflow
if count(open within 7d) >= 1 { ... }
```

**`until` — wait or loop.** Checked repeatedly, while waiting or before each
iteration.

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

```sendflow
wait up to 7d until exists(activity.order)
repeat up to 3 every 2d until count(click within 2d) >= 1 { ... }
```

## When each is evaluated

This is the only thing about conditions that is genuinely SendFlow's, rather than
SendQL's, and getting it wrong is how campaigns misbehave.

| Where | Evaluated | Against |
|---|---|---|
| `enter ... where` | once, at the moment the trigger fires | the contact, at that instant |
| `exit ... when` | **before every step**, continuously | the contact, right now |
| `if` | once, when control reaches it | the contact, right now |
| `wait up to ... until` | repeatedly, until it is true or the bound expires | the contact, right now |
| `repeat ... until` | before each iteration | the contact, right now |

The one to internalise is `exit ... when`. It is not checked where you wrote it —
it is checked *everywhere*. A contact halfway through a five-day wait, inside the
second iteration of a repeat, will still leave the instant their exit predicate
becomes true.

That is what lets you write campaigns that read as goals rather than as defensive
checks:

```sendflow winback.flow
workflow "Winback" v1 {
  enter on segment "inactive-90d"
  exit "reactivated" when count(open within 14d) >= 1

  send "we-miss-you" via topic "marketing"
  wait 3d
  send "last-call" via topic "marketing"
}
```

Nobody who opens the first email receives the second, and there is not a single
`if` in the file.

## Absence applies here too

The [absence contract](/docs/sendql/absence) does not stop at the workflow
boundary. This branch does **not** take the `then` arm for a contact who has no
`plan` attribute at all:

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

```sendflow
if attr.plan != "trial" { ... }
```

The analyzer warns about it inside a workflow exactly as it does in a standalone
segment:

> `warning: predicate on 'attr.plan' excludes contacts where it is unknown`

In a workflow this matters more, not less: an absent attribute silently routes
someone down the `else` arm, and "why did the free users get the pro email"
usually resolves to exactly this.

Say what you mean:

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

```sendflow
if has attr.plan and attr.plan != "trial" { ... }
```

## Events, activities, and the workflow's own sends

Inside a workflow, event terms have a natural and very useful reading: they let a
campaign react to the mail it just sent.

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

```sendflow
if not exists(open where template = "welcome") {
  send "welcome-reminder" via topic "onboarding"
}
```

*They did not open the welcome email, so nudge them.* The `open` event and the
`template` property are ordinary SendQL — there is no special "did they open the
last message" construct, and none is needed.

Remember which side of the line each source falls on:

- `open`, `click`, `bounce`, `delivery` — **events**, about your email
- `activity.login`, `activity.order` — **activities**, about your product

A workflow that waits on `exists(order)` will fail to analyze with
`unknown event "order"`. You wanted `exists(activity.order)`.

## One line, one predicate

The formatter prints every embedded predicate on a **single line**, however you
wrote it. A condition long enough to want wrapping is a condition that probably
wants to be a saved segment instead:

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

```sendflow
if in segment "high-value-at-risk" { ... }
```

That composes, it is testable on its own, and it is one place to change when the
definition moves.
