English
Approval
Suspends the execution until a human approves or rejects. Request sent by email, with a timeout.
The Approval node puts a person in the loop. It suspends the run, asks for a decision, and resumes on approved or rejected. Nothing is kept in memory while it waits: the request, its deadline and its reminder are stored in the database, so a restart of the server loses nothing.
The node itself does nothing outside the product: the action you want to validate (sending a reply, writing to a CRM) goes on the approved branch. Because refusals are a branch too, you can handle them (label the email, notify a colleague) instead of leaving a dead end. To wait for time or for an external event rather than for a person, use Wait. See also Review and approvals.
Who decides, and how.
- Who receives the request. The member the run acts for, that is the owner of the workflow. The request is an email sent from that member's mailbox to the same address: the mailbox of the triggering email when there is one, otherwise the member's first connected mailbox. Its subject starts with
[Approval], followed by the question. - Deciding from the email. The email contains two buttons, Approve and Reject. Each one is a link carrying a secret, single-use token:
<PUBLIC_BASE_URL>/api/v1/approvals/t/<token>/approve(or/reject). Clicking it records the decision and shows a confirmation page, without any sign-in. The links work only once: a second click shows the decision that already stands. - Deciding in the app. Pending requests appear in Activity → To do, with Approve and Reject buttons, and a notification is posted when a request arrives. The API exposes the same queue (
GET /api/v1/approvals) and decision (POST /api/v1/approvals/:id/decide). - Reminder. By default a reminder email, prefixed
Reminder:, is sent halfway through the timeout. The administrator sets this fraction withAPPROVAL_REMINDER_FRACTION(0disables reminders; see environment variables). - Timeout. When nobody answers in time, the product decides alone according to "Without an answer": Reject by default, so a forgotten request never turns into a send. The timeout is between 1 and 720 hours (30 days), 48 hours by default.
A decision is final: the first one recorded wins, whichever channel it came from.
At a glance
- Type:
flow.approval· version 1 - Category: Logic
- Kind: Step — one stage of a run
- Effect: No external effect (
none) — nothing is written outside Mankomail; safe to replay - Needs a carrier email: No
- Connection: None
- Inputs:
main - Outputs:
approved,rejected
Parameters
title
Question — The sentence the approver reads first. Accepts {{ }} expressions — name the client, the amount, the subject.
- Type: Text (
string) - Required: Yes
- 200 characters at most
- Example:
Approve sending the quote to {{ email.from.name }} - Expressions:
{{ }}accepted
details
Details — What the approver needs in order to decide: the drafted reply, the extracted amount, the history. Accepts {{ }} expressions.
- Type: Long text (
text) - Required: No
- Default:
""(empty) - 5000 characters at most
- Example:
Proposed draft: {{ data.compose_1.message.bodyText }} - Expressions:
{{ }}accepted
timeoutMs
Response time limit — After this delay the workflow decides on its own, per the setting below. A run never waits forever.
- Type: Duration (
duration) - Required: No
- Default: 2 days (
172800000) - Stored in milliseconds, typed in hours or days
- From 1 hour to 30 days
onTimeout
Without an answer
- Type: One choice (
options) - Required: Yes
- Default:
reject - Options:
reject— Reject: The "rejected" branch is taken. The default: a forgotten request must not become a send.approve— Approve: The "approved" branch is taken. Reserve this for actions without consequences.
Outputs
approved— Taken when the request is approved — or when the timeout expires with "Without an answer" set to Approve. In a test run, always taken.rejected— Taken when the request is rejected — or when the timeout expires with "Without an answer" set to Reject (the default).
Data produced
What this node adds to the run data, and how to read it in an expression. <step> stands for the step key: the node name turned into an identifier (see Data and expressions).
{{ data.<step>.status }}—string.pending: the state written when the request was sent. It is not updated by the decision; the branch taken tells you the outcome.{{ data.<step>.title }}—string. The question, rendered.{{ data.<step>.details }}—string. The details, rendered. Absent when empty.{{ data.<step>.decision }}—string. Test runs only: alwaysapproved.{{ data.<step>.simulated }}—boolean. Test runs only:true.{{ data.<step>.timeoutMs }}—number. Test runs only: the timeout that would have applied, in milliseconds, after bounds.{{ data.<step>.onTimeout }}—string. Test runs only:rejectorapprove, the decision the timeout would have taken.{{ data.<step>.effect }}—string. Test runs only: a sentence describing the request that would have been sent.
Example
Check an AI-drafted reply yourself before it goes out. After Compose (AI), add an Approval node:
title: Send the quote to {{ email.from.name }}?
details: Proposed reply:
{{ data.compose.message }}
timeoutMs: 86400000 (24 h)
onTimeout: rejectConnect approved to Send and rejected to a Flag step. The member receives [Approval] Send the quote to Jane Doe?, clicks Approve on their phone, and the run resumes on approved.
Tips
- In test runs the node does not wait for anyone: it takes
approvedimmediately, sends nothing, and records the request it would have sent (simulated,effect). - A member with no connected mailbox receives no email; the request can still be decided in the app.
- When sending is suspended by an administrator, the request email is held and goes out when sending resumes. The request is decidable in the app in the meantime.
- Some security gateways open links in incoming emails automatically. If that is the case in your organisation, prefer deciding in the app, and keep "Without an answer" on Reject.
titleanddetailsare templatable: quote the client, the amount and the draft so the approver can decide from the email alone.