# Conditions and variables

How a condition decides whether a workflow starts or which path a contact takes, and how a {{variable}} is filled into the text a workflow sends.

## Getting here

**Automate → Workflows**, open a workflow. Conditions live in the trigger's **Trigger Conditions** section and inside the segments of a **Condition** step. Variables go into any message box, with the **Variable** button beside it.

## Before you start

- An **owner**, **admin** or **agent** can add conditions and variables and save the workflow. Publishing that workflow — the thing that makes it run on live conversations — is an **owner** or **admin** action.
- Building and running workflows depends on your plan; see [pricing](/pricing).
- Nothing outside Inchat has to be connected. A condition or a variable only reads what the trigger, an earlier step or the contact already carries.
- Choose the trigger first. The list of fields a condition can read depends on it.

## Set up

1. For a rule on the whole workflow, open the trigger and expand **Trigger Conditions**. For a rule that sends contacts down different paths, open a **Condition** step and use one of its segments.
2. Click **Add condition**. Choose a category, then the field, then how to compare it, then the value.
3. With more than one condition in a list, say how they combine: **Match all conditions** or **Match any condition** on the trigger, **all match (AND)** or **any matches (OR)** on a segment.
4. To put a variable in text, click into a message box, press **Variable**, search and pick the field. The box gets a token such as `$contact.name`.
5. To cover a contact with no value, add a fallback after the name: `$contact.name||there`.
6. Save, then publish. A saved but unpublished workflow starts for nobody.

## Fields

| Field | What it means | Default | Limits |
| --- | --- | --- | --- |
| Category | Groups the field menu: **Incoming message**, **Question answer**, **Contact**, **Contact field**, **Channel**, **Conversation**, **Trigger details**, **AI reply**, **HTTP request**, **Quote** and **Other**. | — | A category with nothing to offer at this point of the workflow is not listed. **Other** always is. |
| Field | The one fact to test, such as the message text, a tag list, the channel or an order status. | — | Shown as a menu only when the category has more than one field. In **Other** you type the path yourself. |
| Comparison | How to test it. The menu offers only the comparisons that fit the field: text, number, date, yes/no, a fixed set of answers, or tags. | — | — |
| Value | What to compare against. A field with a fixed set of answers shows them as a menu, and a yes/no field shows **Yes** and **No**. | — | Hidden for **exists** and **does not exist**, which need none. |
| Match all conditions / Match any condition | How the conditions on the trigger combine. **Match all conditions** needs every row to pass; **Match any condition** needs one. | Match all conditions | Offered only once the trigger has more than one condition. |

## How it works

- A condition is one row of four menus — category, field, comparison, value — saved as a field path, a comparison and a piece of text.
- The category menu lists **Incoming message**, **Question answer**, **Contact**, **Contact field**, **Channel**, **Conversation**, **Trigger details**, **AI reply**, **HTTP request**, **Quote** and **Other**, in that order, leaving out any that has nothing to offer.
- **Contact**, **Channel** and **Conversation** fields are offered for every trigger. **Incoming message** fields are offered for triggers that carry a message, and each family of triggers adds its own fields under **Trigger details** — the comment text for a comment, the order number for an order, the tag that changed for a tag update.
- **Question answer**, **AI reply**, **HTTP request** and **Quote** appear only when a step that produces them, such as Ask a Question or AI Reply, sits earlier on the path.
- **Other** takes any field path you type, for example `http.data.status`, and saves it without checking that anything ever fills it.
- The comparison menu depends on the field. Text offers **is equal to**, **is not equal to**, **contains**, **does not contain**, **exists** and **does not exist**. A number swaps the two **contains** options for **is greater than**, **is greater than or equal to**, **is less than**, **is less than or equal to** and **is between**.
- A date offers **is after**, **is before**, **is between**, **exists** and **does not exist**. A yes/no field offers only **is equal to**. Tags offer **has tag**, **does not have tag**, **has any of**, **has all of** and **has none of**.
- The value is always saved as text. **exists** and **does not exist** are the two comparisons that take no value.
- **exists** asks whether the field is present at all, not whether it holds any text: a field holding empty text counts as set.
- Text is compared without regard to capital letters or spaces around it, so **is equal to** `A` also matches a reply of `a` or ` A `, and **contains** `Inseller` also matches `INSELLER`.
- **is equal to** on a number field compares numbers: the typed text `404` matches a lookup that returned the number 404. Two pieces of text are still compared as text, so `007` does not equal `7`.
- **is equal to** on a yes/no field compares the real yes or no, so choosing **Yes** matches a contact who agreed to marketing messages.
- **contains** on text matches a piece anywhere inside it. On a list such as tags it matches one whole entry, so `vip` matches a tag called `VIP` but not one called `vip-plus`.
- **is greater than**, **is greater than or equal to**, **is less than** and **is less than or equal to** read both sides as plain numbers.
- **is between** includes both ends and accepts them in either order. Written as text it is two values split by a comma or by two dots; the builder gives you two boxes instead.
- **is before** and **is after** compare moments in time, read from a date picker or from a stored date and time.
- **has any of**, **has all of** and **has none of** take a list separated by commas, compared without regard to capital letters, against a tag list or a single value.
- **Trigger Conditions** decide whether the workflow starts at all: the trigger fires, the conditions are read against that event, and the workflow only runs if they pass. With no conditions the workflow starts on every event of that trigger.
- **Match all conditions** is the default. **Match any condition** starts the workflow when one row passes, which is how a list of keywords is written as one row each.
- The ad triggers have no **Trigger Conditions** section, because the ad the contact came from is the entry. Conditions saved on one earlier still run, and the drawer names them with a **Remove them** button.
- Each segment of a **Condition** step has its own list of conditions and its own all-or-any choice, checked from the top: the first segment whose conditions pass takes the contact and the rest are not read. The Condition step's own article covers the paths themselves.
- A run carries its values with it. After a Wait or a question, later steps read the values captured when the run paused, not a fresh copy of the contact.
- The one value a run re-reads at the step is **Agreed to marketing messages**, because a contact can withdraw it during a long wait.
- A variable is written `{{contact.name}}` or `$contact.name`, and either is replaced with that field's value at the moment the step runs. The **Variable** button writes the `$` spelling.
- The **Variable** list offers the same fields the condition menu does, leaving out the channel, yes/no fields and the request status fields, plus the whole response of an HTTP Request.
- Braces work with any field path. The `$` spelling is filled in only when the name starts with one of a fixed list of families — contact, conversation, message, channel, ad referral, answer, quote, AI, HTTP and saved answers among them — so a price such as `$50` stays as typed.
- A variable with nothing to show takes its fallback: `{{contact.name||there}}` or `$contact.name||there`. The fallback is used when the field is missing or empty, and a value of `0` or `false` still shows as it is.
- In the braces spelling a fallback runs up to the closing braces and may hold spaces. In the `$` spelling it stops at the first space, so it can only be one word.
- A field that holds a list or a group of values, such as a contact's tags or a whole HTTP response, is written out as JSON, so the placeholder shows you what you got rather than vanishing.
- A single value that is too long is cut short and ends in `…`, so one lookup cannot take over the message.
- Variables are filled into message text, file links and captions, template variables, internal comments, task titles and notes, and the value saved by Save to Contact Field.

## When it fails

- A field the trigger or an earlier step never filled has no value. **is equal to**, **contains**, the number and date comparisons and **exists** then read as not matching, and nothing is reported.
- The opposite comparisons match a field with no value: **is not equal to**, **does not contain**, **does not exist**, **does not have tag** and **has none of** all pass. A mistyped path on one of them passes for every contact.
- When a trigger's conditions do not pass, the workflow does not start and nothing is sent to the contact. Open the workflow's **Activity** tab and look under **Not triggered**: the line reads **Conditions did not match — the message read** followed by the text it saw.
- The workflow panel on the conversation shows the same reason with the workflow's name in front. A draft or stopped workflow is never a candidate, so nothing is listed for one until it is published.
- When a trigger's conditions cannot be evaluated the workflow is skipped, and the **Not triggered** line reads **Its conditions could not be evaluated (an error was logged).**
- In a **Condition** step, a segment whose evaluation fails counts as not matching, and a contact that matches no segment goes down the **None** lane.
- A number, date or **is between** comparison whose text cannot be read as a number or a date does not error: it does not match.
- A variable with nothing to show and no fallback is replaced with nothing. The message still sends with a gap where it was, and the placeholder text is not left in. Some fields log a warning naming the variable and the conversation when it comes up empty: the text, caption or header of a message sent to the customer, a **Send WhatsApp Template** step's variables, and an internal note's text. Everything else is filled the same way but logs nothing, including a task's title and notes, a file URL, and a **Start a WhatsApp Chat** step's phone number and template variables.
- Saving is refused when a comparison other than **exists** and **does not exist** has an empty value: **The condition on "<field>" needs a value.**
- A **Condition** segment with no conditions is refused with **“Segment 1” needs at least one condition** (the name is the segment's own, or Segment and its number).
- Publishing a **Message received** workflow with no conditions and no channel chosen is refused with **This workflow runs on every inbound message in the workspace. Add at least one condition (for example "Message sender is customer" plus a keyword or a channel) before publishing.**

## Limits

- The comparisons are the fixed set above. There is no arithmetic, no wildcard or pattern matching, and **contains** is a plain match.
- The value is compared exactly as typed. A `{{...}}` variable typed into a value is not filled in, so a condition cannot compare one field against another.
- One list is all-of or any-of, never a mix, and lists do not nest. Two rules joined differently need two segments or two steps.
- Only the trigger's list and a segment's list have the all-or-any choice. An older Branch step, which is no longer in the add-step menu but still opens and edits, checks all its conditions and sends the rest to its **Else** path.
- A manual **Run a workflow** does not read the trigger's conditions on the message or on trigger details, because there is no triggering message. It checks only the **Contact**, **Channel** and **Conversation** conditions, requires all of them even when the trigger says **Match any condition**, and lets you go past a mismatch with **Enrol anyway**.
- Contact values in a run are frozen at the moment it started and are not re-read as the run goes on. A **Tag Contact** or **Save to Contact Field** step earlier in the SAME run does not become visible to a later **Condition** in that run, any more than a tag a teammate adds during a **Wait** does. Only **Agreed to marketing messages** is re-read.
- Numbers are read as plain numbers. Text such as `1,200` or a value with a currency sign is not a number, so a greater-than or less-than on it does not match.
- A variable is not filled into the address, headers or body of an HTTP Request; those are sent as typed.
- The `$` spelling cannot carry a fallback with a space in it; use the braces spelling for a phrase.
- The `$` spelling is not filled in for every field the **Variable** button lists. Deal, comment, payment, link, video and shipment fields, and the changed-tag and changed-field details, are outside the list, so `$deal.value` goes out as typed. Write `{{deal.value}}` for those.
- The builder limits how many conditions one list can hold and refuses to save a longer one.

## Best practices

- Narrow the trigger itself where it offers a scope, such as the channels it listens on, and use conditions for the rest, so a **Message received** workflow does not read every message in the workspace.
- Write a keyword rule as one row per keyword with **Match any condition**, not as one row with all the words in it.
- Give every variable that may be empty a fallback, and read the message as the contact will: `Hi {{contact.name||there}}` reads well with or without a name.
- Prefer the positive comparison when the field might be missing: **has tag** `vip` simply does not match a contact without the field, while **does not have tag** `vip` matches them.
- When a workflow does not start, open **Activity** and read what the trigger actually saw before changing a condition that seems not to match.

## Use cases

- Start a workflow only for messages that mention a product: **Incoming message → Message text** **contains** the product name, with another row for each spelling and **Match any condition**.
- Send a returning customer down a different path: a **Condition** step with a first segment for **Contact → Total orders** **is greater than** a number, and everyone else on **None**.
- Greet Sarah Chen by name without leaving a gap for a contact with no name: `Hi {{contact.name||there}}, thanks for reaching out.`
- Quote a value an **HTTP Request** step already looked up in the next message, such as `Current status: {{http.data.status}}.` The request itself cannot be personalised this way — its address and body are sent exactly as typed, never filled with `{{contact.phone}}` or anything else — so this only works for a lookup that does not need to know who is asking.
- Lock a workflow to one number: **Channel → Channel** **is equal to** the connected number.

## FAQ and troubleshooting

### My condition never matches, though the message looks right. Where do I look?

Open the workflow's **Activity** tab and read **Not triggered**: it shows the text the trigger saw. Then check that the field is one this trigger fills — a field it does not fill has no value and never matches a positive comparison. Capital letters and spaces around the text do not matter.

### Why does a **does not equal** or **does not contain** condition match every contact?

Those comparisons pass when the field has no value. If the path is mistyped, or belongs to another trigger, there is never a value, so the condition is true for everyone. Check the field in the menu, or use the positive comparison.

### Will my customer ever see `{{contact.name}}` in a message?

Not a well-formed one: a field path spelled with only letters, numbers, underscores and dots, like `contact.name`, is replaced with nothing when it has no value, or with its fallback when it has one. A path with a space or a hyphen in it is not recognised as a variable at all and is sent exactly as typed, braces and all — stick to plain field names, and add a fallback such as `{{contact.name||there}}` to avoid a gap.

### Which should I use, `{{contact.name}}` or `$contact.name`?

Either. Both are filled in the same way at the moment the step runs. The **Variable** button inserts the `$` spelling, and the braces spelling is the one to use for a fallback made of several words.

### How do I match any of several keywords?

Add one **contains** row per keyword under **Trigger Conditions** and choose **Match any condition**. In a **Condition** step, choose **any matches (OR)** on the segment.

### Do capital letters matter in a condition?

No. Text is compared without regard to capital letters and without the spaces around it, so `VIP`, `vip` and ` vip ` are the same to a condition.

## Related

- [Condition](https://inchat.inseller.my/help/workflows/condition)
- [Workflows: how a flow runs](https://inchat.inseller.my/help/workflows/workflows-overview)
- [Build a workflow with AI](https://inchat.inseller.my/help/workflows/ai-workflow-builder)
- [Workflow triggers](https://inchat.inseller.my/help/workflows/workflow-triggers)
- [Avoid workflow loops](https://inchat.inseller.my/help/workflows/avoid-workflow-loops)

---

Source files: `src/lib/workflows/condition-fields.ts`, `src/lib/workflows/condition-catalog.ts`, `src/lib/workflows/interpolate.ts`, `src/lib/workflows/dollar-placeholders.ts`, `src/lib/workflows/engine-condition.ts`, `src/lib/workflows/engine.ts`, `src/lib/workflows/field-path.ts`, `src/lib/workflows/live-consent-field.ts`, `src/lib/workflows/pending-payload.ts`, `src/lib/workflows/pending.ts`, `src/lib/workflows/dispatch.ts`, `src/lib/workflows/manual-enrol-guard.ts`, `src/lib/workflows/workflow-input-schema.ts`, `src/lib/workflows/form-payload.ts`, `src/lib/workflows/http-request.ts`, `src/lib/workflows/engine-message-actions.ts`, `src/lib/workflows/engine-record-actions.ts`, `src/components/workflows/ConditionBuilder.tsx`, `src/components/workflows/VariablePicker.tsx`, `src/components/workflows/WorkflowTriggerConfigDrawer.tsx`, `src/components/workflows/WorkflowStepConfigDrawer.tsx`, `src/components/workflows/WorkflowHistory.tsx`, `src/components/inbox/RunWorkflowDialog.tsx`
