English
Triggers and conditions
A trigger is the node that starts a workflow. It sits on the left of the canvas, has no input, and never runs as a step: when it fires, it creates an run and hands it to the nodes linked after it. This page lists every trigger, what makes each one fire, what it brings to the run, and how the conditions of the email trigger are written.
How the rest of the graph runs is explained in Workflows.
The triggers at a glance
| Type | Name in the editor | Fires when | Triggering email | Data it brings |
|---|---|---|---|---|
trigger.email | Email received | a new email matching the conditions arrives in one of your mailboxes | yes | the email, as email |
trigger.webhook | Webhook received | an HTTP POST reaches the workflow's URL | no | the JSON body, as data.webhook |
trigger.called | Called by a workflow | another workflow runs it with "Call a workflow" | the caller's, if it has one | the caller's data, as data.input |
trigger.schedule | Schedule | the clock reaches the next occurrence | no | data.schedule |
trigger.manual | Manual run | you run it on an email | yes | the email, as email |
trigger.airtable | Airtable row changed | a check finds new or changed rows | no | data.airtable |
trigger.notion_changes | Notion entry changed | a check finds new or changed entries | no | data.notion |
trigger.notion | Notion — event | Notion sends an event to the workflow's URL | no | data.notion |
trigger.mynotary | MyNotary — event | MyNotary sends an event to the workflow's URL | no | data.mynotary |
trigger.yousign | Yousign — event | Yousign sends a signed event to the workflow's URL | no | data.yousign |
A trigger only fires once the workflow is published, except trigger.manual, which never fires on its own (you start it). Pausing a workflow stops email, schedule and polling triggers without unpublishing it.
Several triggers in one workflow
A workflow can carry several triggers. Each run remembers the trigger that started it, and only the branches linked after that trigger run. A workflow with an email trigger and a webhook trigger runs only its webhook branches when a call arrives.
trigger.emailcan be repeated as often as you like: each copy is another set of conditions. When one email satisfies two email triggers of the same workflow, the workflow still runs once on it, through the trigger whose node id comes first in alphabetical order.trigger.webhook,trigger.called,trigger.mynotary,trigger.notionandtrigger.yousigncan appear only once per workflow (duplicate_triggerat publication). The URL and the ability to be called belong to the workflow, not to a node.- A workflow has a single URL. Use one URL-based trigger per workflow (
trigger.webhookor one integration event trigger), not several of different types. - A disabled trigger does not fire. A workflow whose triggers are all disabled still publishes, with the warning
all_triggers_disabled.
The triggering email
Some triggers bring an email to the run, others do not. This decides which nodes can be used.
| What the triggers bring | Triggers | Effect on nodes that act on the email |
|---|---|---|
| Guaranteed — every run has an email | only trigger.email and/or trigger.manual | allowed |
| Inherited — the email of the calling workflow, if it has one | trigger.called, possibly with email or manual triggers, and none of the triggers below | allowed, with the warning carrier_email_inherited: they work when the caller was started by an email, and fail when it was started by a schedule or a webhook |
| Absent — at least one trigger starts runs with no email | any of trigger.webhook, trigger.schedule, trigger.airtable, trigger.notion_changes, trigger.notion, trigger.mynotary, trigger.yousign | refused at publication with carrier_email_required |
All triggers count, disabled ones included: adding a schedule trigger to an email workflow, even disabled, makes the email absent for the whole workflow.
What "acts on the email" means:
- Nodes that need the email:
mail.move(Move) andmail.flag(Flag). They have nothing to act on without one. - Settings that need the email: "Reply in thread" on
mail.compose(Compose) andai.compose(Compose (AI)) — unticked by default when the email is absent; "Reply in thread" (reply) onmail.send(Send) — use "New thread" or the default instead; and the "A reply lands in the thread" early wake-up offlow.wait(Wait).
A disabled node is never checked: it does not run. To work on an email from a webhook or a schedule workflow, look it up from the data (for example by a reference stored in a table) rather than relying on a triggering email.
Email received (trigger.email)
trigger.email starts the workflow when a new email that matches its conditions arrives in one of your mailboxes. A new workflow starts with this trigger already in place.
Which mailboxes. Publishing arms the trigger on every mailbox you own that is not disconnected. A mailbox you connect later is armed automatically, without republishing. There is no per-trigger mailbox setting.
Which emails can reach a trigger. Before any condition is evaluated, an email must:
- be new: delivered by synchronisation after the mailbox was connected. The history imported when you connect a mailbox never starts workflows, and neither does an old email that reappears (restored from the trash, relabelled);
- not be in Sent, Spam, Trash or Drafts;
- be in scope: not excluded by the organisation's or your own scope rules;
- not be stopped by a guardrail: emails sent by Mankomail itself (for example a workflow's own reply coming back through sync), automatic replies (out-of-office and similar), mailing-list and newsletter emails, and emails from no-reply addresses never reach triggers.
Because of the guardrails, conditions such as signals.isMailingList equals true or signals.isAutoReply equals true never match in practice. The signal fields are still useful to exclude, for example signals.isAutoReply equals false.
Existing emails are not processed. Publishing a workflow does not apply it to emails already received: only emails that arrive afterwards are checked. To run a published workflow on an email you already have, use a manual run.
One run per email and per version. The same email delivered twice (push and poll, retries) starts a given published version only once.
When several workflows match the same email, your trigger policy decides, in your scope settings under "My trigger policy":
- "Every workflow that matches" (default): each matching workflow gets its own run;
- "The first match, and only that one": workflows are tried in priority order and only the first match runs. The order is the "Rank" in each workflow's "Settings" tab, from 0 to 9999, lowest first; workflows with no rank come after, in creation order.
The trigger also shows a "When several workflows match" setting (multiMatch: inherit, all, priority). The policy actually applied is the one from your settings, for every workflow.
Email trigger conditions
The conditions of trigger.email (parameter conditions) are a tree of groups and leaves:
- a group is
{ "all": [ … ] }(every child must hold) or{ "any": [ … ] }(at least one must hold), and groups can be nested; - a leaf is
{ "field": …, "op": …, "value": …, "caseSensitive": … }.
No conditions means the trigger listens to every email that can reach it.
json
{
"all": [
{ "field": "from.domain", "op": "matchesDomain", "value": "client.example" },
{ "field": "attachments.mimeTypes", "op": "contains", "value": "application/pdf" },
{
"any": [
{ "field": "subject", "op": "contains", "value": "invoice" },
{ "field": "subject", "op": "contains", "value": "facture" }
]
}
]
}This fires for an email from client.example or one of its subdomains, with a PDF attachment, whose subject contains "invoice" or "facture" in any case.
In the editor, the trigger panel edits one level: choose "All" or "Any" under "How conditions combine", then add conditions (Field, Operator, Value). Nested groups written through the API are kept and shown as "nested groups kept — not editable here". "Ready-made conditions…" inserts common ones, for example "Has at least one attachment", "Has a PDF attachment" or "Is not an auto-reply".
Condition fields
Each field has a type, which decides the allowed operators and the type of value.
| Field | Label in the editor | Type | Content |
|---|---|---|---|
from.email | Sender (address) | text | the sender's address |
from.name | Sender (name) | text | the sender's display name |
from.domain | Sender (domain) | text | the sender's domain, lower case, without trailing dot |
to.emails | To (addresses) | list | the To addresses |
to.domains | To (domains) | list | the To domains |
cc.emails | Cc (addresses) | list | the Cc addresses |
cc.domains | Cc (domains) | list | the Cc domains |
recipients.emails | All recipients (addresses) | list | To + Cc + Bcc — use it for "sent to this alias" |
recipients.domains | All recipients (domains) | list | the domains of To + Cc + Bcc |
replyTo.emails | Reply-To (addresses) | list | the Reply-To addresses |
subject | Subject | text | the subject |
bodyText | Message body | text | the full plain-text body |
receivedAt | Received at | text | ISO 8601 date-time in UTC |
sentAt | Sent at | text | ISO 8601 date-time in UTC, when the email carries one |
attachments.count | Attachment count | number | the number of real attachments; inline images such as signature logos do not count |
attachments.filenames | Attachment names | list | the file names of the real attachments |
attachments.mimeTypes | Attachment types | list | the MIME types of the real attachments (application/pdf) |
folderLabels | Folders / labels | list | the IMAP folders or Gmail labels the email carries |
flags.seen | Seen | yes/no | read |
flags.flagged | Flagged | yes/no | flagged / starred |
flags.draft | Draft | yes/no | draft |
flags.sent | Sent | yes/no | sent |
signals.isAutoReply | Auto-reply | yes/no | detected as an automatic reply |
signals.isNoReply | No-reply address | yes/no | sent from a no-reply address |
signals.isMailingList | Mailing list | yes/no | detected as a mailing-list or bulk email |
signals.isFromSelf | Sent by me | yes/no | sent by one of your own addresses |
header:<name> | — | list | every value of a raw header, for example header:list-id or header:x-mailer |
header:<name> is available in the document and through the API, not in the editor's field list. The header name must be written in lower case letters, digits and hyphens (header:x-priority, not header:X-Priority).
Condition operators
| Operator | Label in the editor | Text fields | Number fields | Yes/no fields | List fields |
|---|---|---|---|---|---|
eq | equals | ✓ | ✓ | ✓ | ✓ |
neq | does not equal | ✓ | ✓ | ✓ | ✓ |
contains | contains | ✓ | — | — | ✓ |
startsWith | starts with | ✓ | — | — | ✓ |
endsWith | ends with | ✓ | — | — | ✓ |
gt | is greater than | ✓ | ✓ | — | — |
lt | is less than | ✓ | ✓ | — | — |
exists | is set | ✓ | ✓ | ✓ | ✓ |
matchesDomain | has domain | ✓ | — | — | ✓ |
Value types. Text and list fields take a text value; number fields a number (0, not "0"); yes/no fields true or false. matchesDomain always takes a text value. exists takes no value (or true) to test that the field is set, and false to test that it is not.
How each operator behaves:
- Text comparisons ignore case by default but respect accents:
cafe.frandcafé.frare different. Add"caseSensitive": trueto a leaf to compare exactly. Unicode spellings of the same character compare equal. - On list fields, operators hold when at least one element satisfies them (
to.domainscontainsclientholds if any To domain contains it).neqis the exception: it holds when no element is equal, so an empty list satisfies it. matchesDomaincompares domains: the value is a domain or an address (only the part after@is used), and the field holds when its domain is that domain or a subdomain of it.from.emailhas domainclient.examplematchesanna@mail.client.example.gtandlton text compare alphabetically. OnreceivedAtandsentAt, which are ISO dates in UTC, this is a chronological comparison:receivedAtis greater than2026-10-01holds for anything received from 1 October 2026 (UTC).existsholds for a text field that is not empty, a list that has at least one element, and a number or yes/no field that has a value.- A text field the email does not carry (no sender, no
sentAt) makes every operator false, exceptexistswithfalse. List fields are always present, possibly empty.
Condition errors
Publication refuses conditions that cannot be evaluated, with the error invalid_trigger_conditions and one issue per faulty leaf, located by its path in the tree (all[0].any[2]):
| Issue | Meaning |
|---|---|
unknown_field | the field is not in the list above |
op_not_supported | the operator is not allowed on this field type (for example contains on attachments.count) |
missing_value | the operator needs a value |
invalid_value_type | the value has the wrong type ("0" instead of 0, a non-boolean for exists) |
empty_group | a group has no child: an empty all would match every email and an empty any none |
Conditions are checked on disabled email triggers too, so a trigger can be re-enabled safely.
Condition examples
Has at least one real attachment:
json
{ "all": [ { "field": "attachments.count", "op": "gt", "value": 0 } ] }Sent to a given alias, wherever it appears (To, Cc or Bcc):
json
{ "all": [ { "field": "recipients.emails", "op": "eq", "value": "invoices@company.example" } ] }From one of two customers, and not an automatic reply:
json
{
"all": [
{ "any": [
{ "field": "from.domain", "op": "matchesDomain", "value": "client-a.example" },
{ "field": "from.domain", "op": "matchesDomain", "value": "client-b.example" }
] },
{ "field": "signals.isAutoReply", "op": "eq", "value": false }
]
}Subject starting exactly with an upper-case reference:
json
{ "all": [ { "field": "subject", "op": "startsWith", "value": "REF-", "caseSensitive": true } ] }No Cc at all:
json
{ "all": [ { "field": "cc.emails", "op": "exists", "value": false } ] }The same tree, with fields prefixed email. and data fields data.<path> plus the isEmpty operator, is used by the Condition (If) node later in the graph.
Webhook received (trigger.webhook)
trigger.webhook starts the workflow on an HTTP call. It has no settings.
The URL. Each workflow that uses this trigger has one URL:
<PUBLIC_BASE_URL>/hooks/wf/<token>The token (256 random bits) is created at the first publication, or earlier with "Generate the URL" in the trigger panel. It is shown only once: only a fingerprint is stored, so nobody can display it again. If you lose it, or if it leaks, use "Generate a new URL": the previous URL stops working immediately. Republishing keeps the same URL. The editor shows the path starting with /hooks/wf/; put your instance address in front of it. Through the API, POST /api/v1/workflows/:id/webhook generates a new token and returns it once.
The token is the authentication: no header or key is required. Treat the URL as a secret.
The request.
bash
curl -X POST "<PUBLIC_BASE_URL>/hooks/wf/<token>" \
-H "Content-Type: application/json" \
-d '{"client": {"name": "Jane Doe", "email": "jane@example.com"}}'- Method:
POSTonly. - Body: JSON, at most 256 KB (262,144 bytes). The body becomes
data.webhook: here{{ data.webhook.client.email }}. An empty body givesnull. Headers and query string are not passed to the workflow.
The responses.
| Status | Body | When |
|---|---|---|
202 | { "executionId": "…" } | the run was created; it runs in the background |
400 | error | the body is not valid JSON |
404 | { "code": "not_found", … } | unknown token, or the workflow is not published, archived, or has no active webhook trigger — all answer the same way |
413 | { "code": "request.payload_too_large", … } | the body exceeds 256 KB |
429 | empty, with Retry-After | too many calls from your address; retry later |
Every call creates a new run: two identical calls are two runs. The run has no triggering email (see the triggering email).
Details: Webhook received.
Called by a workflow (trigger.called)
trigger.called makes a workflow callable by others with the Call a workflow node. It has no settings, subscribes to no email, and fires only when called.
- The caller's working data arrives under
{{ data.input }}. - The run inherits the caller's triggering email when the caller has one; otherwise it has none.
- The workflow must be published to be callable, and only one
trigger.calledis allowed per workflow. - A workflow can also carry email triggers: when called, only the branches after
trigger.calledrun.
Details: Called by a workflow.
Schedule (trigger.schedule)
trigger.schedule starts the workflow at regular times, with no triggering email.
| Setting | Label | Values |
|---|---|---|
mode | Cadence | interval — "Every N minutes" (default), or cron — "Cron expression" |
everyMinutes | Every (minutes) | whole number from 5 to 44,640 (31 days); default 60 |
cron | Cron expression | five fields: minute, hour, day of month, month, day of week |
timezone | Time zone | IANA name such as Europe/Paris; empty means UTC |
Cron expressions accept, in each field, *, a number, a range a-b, a list x,y,z of these, and a step /n (*/15, 0/15, 8-18/2). Months accept JAN–DEC and days of the week SUN–SAT; day of week goes from 0 to 7, both 0 and 7 meaning Sunday. Extensions such as ?, L, W and # are refused. An expression that would fire more often than every 5 minutes is refused.
| Expression | Meaning |
|---|---|
0 8 * * 1 | every Monday at 08:00 |
0 9 * * 1-5 | weekdays at 09:00 |
*/15 8-18 * * MON-FRI | every 15 minutes from 08:00 to 18:45 on weekdays |
30 7 1 * * | the 1st of each month at 07:30 |
The cron expression is read in the chosen time zone, daylight-saving changes included. The trigger panel shows the "Next runs" computed by the server. Intervals are aligned on fixed points in time rather than on the publication time: every 60 minutes fires on the hour (UTC).
What the run receives, under data.schedule: plannedFor (the scheduled time, rounded as requested, ISO 8601), firedAt (the actual time) and timezone.
Reliability. Each occurrence creates at most one run. If the service was down at the scheduled time, only the last missed occurrence is caught up, within a few minutes; earlier ones are skipped rather than fired in a burst. A paused workflow skips its occurrences.
Publication refuses an unusable schedule with invalid_schedule and one of: cron_missing, cron_field_count, cron_field_syntax, cron_too_frequent, interval_out_of_range, unknown_timezone.
Details: Schedule.
Manual run (trigger.manual)
trigger.manual marks a workflow meant to be started by hand. Nothing arms it: publishing shows the warning trigger_not_armed, which here only reminds you that you start it.
A manual run executes the published version for real, on one email:
- in the webmail, open the email and choose "Run a workflow", then the workflow ("REAL run of the published version: side effects go out for good");
- through the API,
POST /api/v1/workflows/:id/runwith{ "messageId": "…" }, and an optionalclientTokenso a repeated request creates only one run.
Any published workflow can be run this way, with or without trigger.manual. If the workflow has an active trigger.manual, only the branches after it run; otherwise every branch leaving a trigger runs. The email must belong to one of your mailboxes. To try a draft without effects, use test runs instead.
Details: Manual run.
Integration triggers by polling
trigger.airtable and trigger.notion_changes ask the service "what is new since last time?" at a regular interval, and create one run per new or changed item.
| Trigger | Watches | Interval ("Check every (minutes)") | Data |
|---|---|---|---|
trigger.airtable (Airtable row changed) | rows of an Airtable table, optionally a view and an extra formula; needs a "Last modified time" (or "Created time") field | 5 to 1,440 minutes, default 15 | data.airtable: baseId, tableId, recordId, changedAt, fields |
trigger.notion_changes (Notion entry changed) | entries of a Notion database: created or changed, new only, or changed only | 5 to 1,440 minutes, default 15 | data.notion |
- Starting point: the first publication starts watching from now. Rows or entries that existed before are not replayed, and republishing never resets the position.
- No loss, no duplicate: each item creates one run, even if a check is repeated after an incident.
- A bounded batch per check: what does not fit is picked up by the next check.
- The instance can impose a slower minimum (
INTEGRATION_POLL_MIN_MINUTES, 5 minutes by default): a trigger never checks faster than that. - Errors: a temporary error (rate limit, outage) changes nothing and the next check retries; a permanent one (revoked connection, deleted base) sends you one notification per outage.
- A paused workflow skips its checks.
Details: Airtable row changed, Notion entry changed, and the Airtable and Notion integrations.
Integration triggers by webhook
trigger.mynotary, trigger.yousign and trigger.notion receive events on the same workflow URL as trigger.webhook (<PUBLIC_BASE_URL>/hooks/wf/<token>, created and shown once in the same way). Each carries a connection to the service and an "Events" setting.
| Trigger | Default event | Origin check | Duplicates |
|---|---|---|---|
trigger.mynotary (MyNotary — event) | signature_completed | the URL token only (MyNotary does not sign) | not deduplicated: a delivery repeated by MyNotary creates a second run |
trigger.yousign (Yousign — event) | signature_request.done | signature x-yousign-signature-256 checked against the connection's secret | deduplicated on event_id: Yousign's retries start nothing new |
trigger.notion (Notion — event) | page.properties_updated | signature X-Notion-Signature checked against the connection's verification token | deduplicated on the event id |
- Event filter: only the ticked events create runs. Others receive
202and create nothing, so the sender does not retry. With nothing ticked, every event is accepted. - A rejected signature answers the same
404as an unknown token. - Subscriptions are created on the service side: through the integration node's API operation for MyNotary and Yousign (or by hand in Yousign), and by hand in Notion's developer portal for Notion. Notion can also be watched without any setup with
trigger.notion_changes. - The body arrives under
data.mynotary,data.yousignordata.notion, exactly as the service sent it. Treat it as untrusted data.
Details: MyNotary — event, Yousign — event, Notion — event, and the MyNotary, Yousign and Notion integrations.
Pausing and arming
| Trigger | Armed by publication | Stopped by pause |
|---|---|---|
trigger.email | yes, on every mailbox you own that is not disconnected | yes |
trigger.schedule | yes, one schedule per active trigger | yes |
trigger.airtable, trigger.notion_changes | yes, one polling rule per active trigger | yes |
trigger.webhook, trigger.mynotary, trigger.yousign, trigger.notion | yes, the workflow URL answers | no |
trigger.called | yes, the workflow becomes callable | no |
trigger.manual | no — you start it | — |
Unpublishing or archiving disarms every trigger: the URL answers 404, schedules and polling stop, and the workflow can no longer be called.