# Conditions (If)

> How an If node tests one field of the run's data, the operators available per field type, the full list of testable fields, and how True/False branching works.

Source: https://joely.io/documentation/automations/conditions
Last updated: 2026-09-25

A condition is the **If** node of the editor: it tests **one field** of the data flowing through the run, and routes the workflow through its **True** output when the test passes or its **False** output when it fails. Add one from the **Conditions** menu of the canvas.

A condition can only test what the nodes **before it** published: the [trigger](https://joely.io/documentation/automations/triggers.md) always publishes the ticket's data, and each [action](https://joely.io/documentation/automations/actions.md) publishes what it did. So a condition placed right after the trigger tests the ticket, while one placed after an action can also test that action's outcome.

## Operators

The operators offered depend on the field's type.

**Text** — subject, form button, message content:

> is · is not · is one of · contains · does not contain · starts with · does not start with · ends with · does not end with · is empty · is not empty · matches regex · does not match regex · exists · does not exist

**Text picked from a list** — statuses, priorities, team members (actor, message author, assignee ID) and roles (actor role). The value is chosen, never typed, so the text-matching operators are not offered:

> is · is not · is one of · is empty · is not empty · exists · does not exist

*Message author type* offers the same operators minus *is empty* and *is not empty*, since every chat message has an author type. A condition saved earlier with another text operator keeps it: the editor still shows it, and the workflow runs as before.

**List** — tags, package names, package IDs typed by hand (a ticket can have several of each):

> contains · does not contain · contains any of · contains none of · is not · is empty · is not empty · length is · length is not · length is greater than · length is less than · length is at least · length is at most

**List picked from a list** — packages, assignees, assignee roles, package categories and package shops. The entries are chosen, never typed, so only membership and emptiness are offered:

> contains · does not contain · contains any of · contains none of · is empty · is not empty

As with text, a condition saved earlier with another list operator keeps it.

**Yes/no** — what an action reports about itself (whether it changed anything, whether a call succeeded):

> is true · is false · exists · does not exist

A few rules worth knowing:

- **Presence operators** (*is empty*, *exists*, *is true*, and their negatives) take no value.
- **Multi-value operators** (*is one of*, *contains any of*, *contains none of*) take a list. On a field with a picker you select several entries; on a field you type into, the box turns into tags — **press Enter after each value**, and a value still being typed is flagged until you do.
- ***Matches regex*** runs the pattern with a linear-time engine, so it is safe against malicious patterns; patterns are capped at 200 characters.
- **Length operators** compare against a whole number (`0` and up).
- A condition on a field the workflow does not provide keeps it a [draft](https://joely.io/documentation/automations/building-workflows.md#drafts) — it can still be saved, but not enabled — and the editor names the node and the missing field so you can fix it.

## Fields every trigger publishes

| Field | Type | Value |
|-------|------|-------|
| Ticket number | Text | Typed |
| Subject | Text | Typed |
| Current status | Text | Picked from the status list |
| Priority | Text | Picked from the priority list |
| Tags | List | Typed |
| Packages | List | Picked from your Tebex catalog |
| Package ID (manual entry) | List | Typed — for a package that was disabled or pulled from the shop, which no catalog list offers |
| Package names | List | Typed |
| Package category | List | Picked from your Tebex categories |
| Package shop | List | Picked from your connected Tebex shops |
| Form button | Text | Typed |
| Assignees | List | Picked from the project's members |
| Assignee roles | List | Picked from the project's roles |
| Customer name | Text | Typed |
| Project name | Text | Typed |
| Actor | Text | Picked from the project's members |
| Actor role | Text | Picked from the project's roles |

### Who performed the action

**Actor** is the person whose action fired the event, for instance the team member who changed the status or linked the package, or the author of the chat message. **Actor role** is the role that person holds in your project: a condition on it runs a branch only when someone with that role acts.

> **Only the shop owner's replies count.**
> *On chat message* → condition **Actor role** *is* `Owner` → the actions that should follow the owner's own replies, and nobody else's.

- **Customers have no role.** They are not members of your project, so *Actor role* is empty when a customer acts, for example when they reply from the portal.
- **No person, no match.** When the event was not caused by a person (an AI reply, another automation, the GitHub sync updating a status), *Actor* and *Actor role* are both empty. A condition such as *Actor role is Owner* is then **False**, so an owner-only branch never runs on those events.
- **A condition guards its branch, not the action itself.** "Only the owner" holds as long as no other path of the same workflow leads to that action.
- **Assignee roles** lists each role once, however many assignees hold it: *contains* `Support` matches as soon as one assignee has that role.

### Package category and shop

**Package category** and **Package shop** list each category and each shop of the ticket's linked packages once, however many packages share it: *contains* `MLO` matches as soon as one linked package is in that category. The shop list offers every Tebex shop connected to your project, including one whose catalog has not synced yet.

- **Packages without a category, or no longer in your catalog, contribute nothing.** *contains* is then **False** for them, and *is empty* is **True** when no linked package has a category (or a shop).
- **One matching package is enough.** On a ticket linked to two packages, one in `MLO` and one in `Vehicles`, *Package category* *contains* `MLO` is **True**. There is no "all packages" operator: a condition cannot require every linked package to be in a category.
- **Unlinking a package fires nothing.** The *Package selected* trigger only reacts to packages being added, so a workflow cannot react to a removal.
- **A ticket with no linked package** (or only a free-text package name) has empty lists.
- **Categories are compared exactly.** A condition on a parent category does not match packages filed in its sub-categories.

## Fields a specific trigger adds

| Field | Type | Trigger |
|-------|------|---------|
| Previous status | Text | Status changed |
| New status | Text | Status changed |
| Ticket source | Text | Ticket creation |
| Message author type | Text | On chat message |
| Message content | Text | On chat message |
| Message author (team member) | Text | On chat message |
| Message ID | Text | On chat message |
| Selected packages | List | Package selected |
| Selected package ID (manual entry) | List | Package selected |
| Selected package names | List | Package selected |
| Selected package category | List | Package selected |
| Selected package shop | List | Package selected |

The *Selected package* fields describe only the packages that were **just** linked, so *Selected package category* *contains* `Vehicles` asks "was the package just chosen a vehicle?", even when the ticket was already linked to packages from other categories. *Package category* and *Package shop* still describe every package the ticket is linked to.

*Message author type* is picked from a list: **Customer**, **Team member**, **Project owner**, **AI assistant** or **Automation**. **AI assistant** is a reply the AI wrote, and **Automation** a message a workflow posted (a message or a package question): neither is a *Team member*, so a condition on *Team member* never matches them. System event lines are not chat messages and never fire the trigger, so there is no value for them.

**Project owner** is a refinement of **Team member**, not a separate kind of author: the owner writes team messages like anyone else. So a condition on *Team member* **also** matches the owner's replies, and *Project owner* is how you single them out. Workflows written before the owner value existed keep behaving exactly as they did.

*Message author (team member)* is picked from the project's members. It is empty when the message was posted by an automation (or by the AI on its own), which is how a workflow tells a person's reply from an automation's.

## Fields an action adds

Place the condition **after** the action to test these.

| Field | Type | Published by |
|-------|------|--------------|
| Posted message ID, Posted message type, Posted message content | Text | Post message |
| Chosen option ID, Chosen option label | Text | Post message, when it offers choices |
| Scheduled closure date | Text | Schedule closure |
| Closure skipped | Yes/no | Schedule closure |
| Linked package IDs, Linked package names | List | Set package, Ask for the package |
| Linked package category, Linked package shop | List | Set package, Ask for the package |
| Package changed | Yes/no | Set package |
| Package unlinked | Yes/no | Unlink package |
| Unlinked package names | List | Unlink package |
| Assignee ID, Assignee name | Text | Assign to a user |
| Assignee changed | Yes/no | Assign to a user |
| New priority, Previous priority | Text | Set priority |
| Priority changed | Yes/no | Set priority |
| Notification sent | Yes/no | Discord notification |
| Notified channel ID | Text | Discord notification, to a channel |
| Direct messages delivered, Recipients skipped | Text | Discord notification, as direct messages |
| Webhook succeeded | Yes/no | Call a webhook |
| Webhook HTTP status | Text | Call a webhook |
| AI confidence | Text | Ask AI |
| AI asked for a human | Yes/no | Ask AI |

## Branching

Connect the condition's **True** output to what should happen when it matches, and its **False** output to what should happen otherwise.

- If a condition evaluates *False* and its False output is **not connected**, that branch stops there and the run is recorded as *Condition not met*. This reproduces a simple filter, and other branches of the same run, if any, still complete.
- Several conditions chained through their **True** outputs act as an **AND**.
- A *Condition not met* run costs nothing against your [monthly quota](https://joely.io/documentation/automations/runs-and-quota.md#monthly-run-quota), so conditions are free to use as filters.

## Example

> **Urgent tickets about one package go straight to the specialist, the others just get tagged.**
> *Ticket creation* → condition **Packages** *contains any of* `Premium Script`, `Premium Script Pro`
> → **True**: **Assign to a user** (the specialist), then **Discord notification** to your team channel
> → **False**: **Post message** as an internal note, so whoever picks the ticket up knows it was not routed.
