WhatsApp approval queue
This guide builds a queue of requests that a person approves or rejects from their own WhatsApp. Customers submit requests in one flow; each request is saved and the approver is notified; the approver opens a list of everything still pending, picks a case, decides, and goes back to the list until it is empty.
It uses nodes that each have their own page: Packages to keep the requests and the decisions, Notify a third party to reach the approver, a Python Executor to work out what is pending, and Interactive Options with options from a list variable to show it.
Why a Queue
Notify a third party on its own handles one case at a time per person. A second notification to the same approver while the first is still pending replaces it, and the first case can no longer be reached from that tap. With two customers submitting a minute apart, the approver would only ever see the second one.
So the notification does not carry the case. It only tells the approver there is something to review, and the approval flow reads the queue itself, every time, from where the requests are stored.
Why a Decisions Package
A package cannot be updated or marked as done. Every time a flow stores a package, a new one is created next to the previous ones; nothing changes the request once it is saved. To record that a request was decided, the approval flow stores another package, a decision, with the same identifier as the request.
A request is then pending when it has no decision stored after it. That rule also covers a customer who submits again after a rejection: the new request is later than the old decision, so it shows up as pending again.
The Pieces
| Piece | What it is |
|---|---|
| Requests package | Key APPROVAL_REQUEST (stored as FLOW_APPROVAL_REQUEST). One package per submission, with the alias set to a unique identifier of the case: a tax id, a national id, an order number |
| Decisions package | Key APPROVAL_DECISION. One package per decision, with the same alias as the request it decides |
| A WhatsApp template | Approved, with a quick reply button, for example Review requests, whose flow is the approval flow |
| Customer flow | Collects the request, stores it and notifies the approver |
| Approval flow | Opens when the approver taps the button. Lists, decides, loops |
Step 1: The Template
Create a template in Templates with a quick reply button and assign the approval flow to that button. A body such as "There is a new request from {{1}} waiting for your review" is enough; the template does not need to describe the case, because the approver will see the whole list. Assign the template its WhatsApp connector, and wait for Meta to approve it.
Step 2: The Customer Flow
After the nodes that collect the request:
- Packages, Store mode. Package key
APPROVAL_REQUEST. Alias: the case identifier, for example{{tax_id}}. It must be unique per case and the same every time the same case is submitted, because it is what ties a decision to its request. JSON fields:name(the customer's or company's name),amount,product, and whatever else the approver needs to see. - Notify a third party. The template from step 1, the approver's number (a Fixed number is the usual choice), and a value for each template variable, for example the customer's name for
{{1}}. You do not need to pass the case under Variables for the target flow: the approval flow reads the queue. - Message. Tell the customer their request is under review.
Connect the Error output of the Notify node too. A request that was stored but whose notification failed is still in the queue, and it will appear the next time the approver opens the list; the notice on the conversation tells you why the notification did not go out.
Step 3: The Approval Flow
This flow starts when the approver taps the template button, in the approver's own conversation.
- Set Variable.
offset, number, literal0. - Packages, Read mode. Key being searched:
APPROVAL_REQUEST. Clear the Customer field, which comes with{{borrower_id}}by default: the approver's conversation is not the customer's, and the queue has to include every customer's requests. List of packages, newest first, at most 50. Result intorequests(array). - Packages, Read mode. The same, with key
APPROVAL_DECISIONand the result intodecisions(array). - Python Executor. Input variables:
requests,decisionsandoffset, with those same keys. Output variable:queue. The code is below. - Set Variable. Three variables from the result:
pending(array) fromqueue.pending,total(number) fromqueue.total,next_offset(number) fromqueue.next. The extraction is needed because the list selector of Interactive Options only offersarrayvariables. - Interactive Options, with Options source set to From a list variable:
- Body: something like "You have
{{total}}pending requests. Pick one to review it." - List button text:
Requests. - Variable with the list:
pending. - Title field:
name. Description field:detail. Identifier field: leave it empty; the elements already carry anid. - Variable for the id of the chosen row:
case_id. Variable for the text of the chosen row:case_name.
- Body: something like "You have
- From Chosen option, a second Interactive Options with fixed options: "
{{case_name}}: approve or reject?", with the options Approve and Reject. Turn on Also save the option key, give the options the keysapprovedandrejected, and save the key intodecision. Both options lead to the next node. - Packages, Store mode. Package key
APPROVAL_DECISION, alias{{case_id}}, JSON fielddecisionfrom thedecisionvariable. Then connect it back to step 1, so the list is read again from the first page, without the case just decided. - From See more, a Set Variable that sets
offsetto the value ofnext_offset, connected back to step 2. - From No match, a Conditional on
totalequal to 0. When it is, a Message ("There are no pending requests") and a Close Conversation. When it is not, the approver typed something that is not in the list: go back to step 2 and show it again.
Connect the Error output of the list, too: it fires when WhatsApp refuses to send it.
The Pending Computation
What it does, in order:
as_listaccepts the read either as a list or as its JSON text, and treats anything else as empty.decided_atkeeps, for each case identifier, the date of its latest decision.- The loop over requests walks them newest first, which is the order the read returns, so the first request seen for an identifier is its latest one; older submissions of the same case are skipped. A request is pending when it is later than the latest decision of its case, or when the case has none.
- Each pending case becomes a row:
id(the identifier, which the list uses to know which case was picked),name(the title) anddetail(the description, cut to 72 characters, the WhatsApp limit, so the list does not have to cut it and warn). offsetis where the page starts. A position that fell out of range, because other cases were decided in the meantime, goes back to the first page.- The result is the page (
pending, from the position onwards), the count of everything pending (total) and the next position (next).
Pages advance by nine because the list shows nine rows plus Ver más when it receives more than ten elements. The page returns everything from the position onwards, so the list always gets more than ten while there are more than ten left, and shows the rest in full on the last page.
Things to Know
The reads bring at most 50 packages each. Newest first, up to 50 requests and 50 decisions. The recipe holds as long as the last 50 requests and the last 50 decisions cover everything still open. For a busier queue, narrow each read (for example with a label per approver or per period) or move the computation to a workflow.
The list is checked against what was sent, not against the queue. If two people approve from the same queue, a case one of them decided can still be picked from the list the other one already has on screen. The second decision is stored as well, and the latest one wins, because it is the one with the later date. If a double decision matters, read the decisions again after the pick and check the case is still pending before storing.
The customer is not told the decision by this recipe. It records the decision. Telling the customer is a separate step that your process decides.
The Ver más row reads the same for everyone. Customers and approvers see Ver más, whatever the language of the editor; the output is called See more on the canvas.
Every write is a new package. Neither the requests nor the decisions are ever overwritten, so the packages of a case are also its history: when it was submitted, each time it was submitted again, and every decision taken on it.