Skip to main content

Human Approval

Category: Operations

Get this pack →

This page is generated from the Air Pipe marketplace. Browse it live to install into your organization.

障 i��ڕj+��Z�w�vƩz�(��,k,�� �~�ݢ��j�kz�h�֜�{ki��)��Zu�e�+h����b�t�y�h»���m��^u�"�*'jwm��춊lN�yȬ���mj��觵��y�'v�+��mN�u��������h�W[z%v)�j����-����v޷��z0��(�K2ji~���N'$z����n��$zv��'m��+yק�&�~��+(�橦�/j_�jg��@)��/z��j)[�܀����j�nnX����{�ڧ�޲���+������N'$z�h�������k��������+�ƥ�����$zv�����$z{b��.��~���E�����!z��yם�k����z�i�ZnW��W���&y�ɪi���u8^q�ez��ʹ٩�j����-����ְk$�z����Z�G��{ay۫i�^��+zz-j�u�'��-���֭���v���-j�]��'��h��jwZ���Z'����,��'z�0Z������������y-����g�jwp��m�쩢�^u���g�����轩l��i�ր��轩Z����o&����y�li����-������kaz*�z�����z����(��z�b� �X�颞�az�$zr-��a{h��+yק�&�j|���h�ا�+\jv���/z�.�殺'gz�������� ��ƛ��b��az��z��I���欢x�����,Š����z�b�D�z��jx�jب�+"��^Qڝج��Z��+z�-jZ�iܚ����ȧz���f�j����x�b��^��(j�$zx.z�v)��+��Ⱬb�w�z-�朅��)ڞ�az�n�Wm��b�L"����'Z�ح��(����^��ޮ�^r�ߊ ��y�e�w���ij��+}������֧j�k���Z��)���^Ƙ�z�a{��)�i�b�wڊ[,��ޯ+a�x jם�x��+a������˜q�y�hz�趻�!��r����^v��z�r�{-y�Z�('�����z�p��]��.z�^��������/z������)䖊$��^���Ry'� ��G�jZ�iܮ��^Ƙ�y֧��^���jx�jب�ƥ������m���NyƮ��b}��z{Z����(�WmzYl�g���.z�"� !��m�G���b��b���W���b�+>����i��ڗ)�N�y�m�)�zf���ay�i��ޮ���wl�v���/jP��+a��'yצ�)�N�jg���0��m��j)kz�Zq�[ʷ���� azX���"������z13�j��nW�U��i�^1槊x>����,����DKZ�z�b����w����r���(���y+z�%����az�������=��0 Z+yǢ����轩kz����0!?QԱ�[�צ�)����r��j�k��������(��+[j�^ƚ+�<�����L�@HO�D

Configuration

approval.yml

name: HumanApproval

docs: true

# Work that needs a person to say yes.
#
# A refund over a threshold, a contract going out, a deploy to production: the flow runs up to
# the decision, then stops until somebody decides. The decision might come in ten seconds or
# next Tuesday, and nothing should be holding a request open in the meantime.
#
# The run is parked in the durable store. Whoever holds the link resumes it, and what they post
# becomes the approval's output — so the actions after it can read who approved and why.
#
# Needs a durable run store: managed has one, self-hosted needs AIRPIPE__DATABASE_URL.

global:
variables:
# Where this engine is reachable, so the approval link resolves for whoever receives it.
public_base: "a|ap_var::PUBLIC_BASE_URL|"
approver_email: "a|ap_var::APPROVER_EMAIL|"
# Who the approval request comes from. Required by the email action, and worth being a
# variable rather than hard-coded so a fork does not send as somebody else's domain.
approver_from: "a|ap_var::APPROVER_FROM|"

interfaces:

# ── Ask for approval, then wait ───────────────────────────────────────────
refunds/request:
output: http
method: POST
summary: Request a refund and wait for a human to approve it
description: >
Answers 202 immediately with a run id; the refund itself only happens once somebody
approves. If nobody does before the deadline the run fails and no refund is issued.
tags: [approval]
request_example: '{"order_id": "A-1001", "amount": 4200, "reason": "damaged in transit"}'

actions:

# The token is the only credential for resuming this run, so it is a uuid rather than
# anything guessable like the order id.
- name: Ticket
json_output: '{"token": "a|uuid|"}'

- name: AskApprover
run_when_succeeded: [Ticket]
email:
from: a|var::approver_from|
to: a|var::approver_email|
subject: 'Approve refund a|body::order_id| — a|body::amount| cents'
text: |
Order: a|body::order_id|
Amount: a|body::amount|
Reason: a|body::reason|

Approve:
a|var::public_base|/_ap/resume/a|org_uuid|/a|Ticket::token|

Nothing happens if you ignore this. The request expires in 7 days and the refund
is not issued.

# The run stops here. It resumes when the link above is called, and the body posted to
# it becomes this action's output.
- name: Approval
run_when_succeeded: [AskApprover]
wait_for_callback:
token: a|Ticket::token|
timeout: "7d"

# Gated on the approval, so an expired request cannot reach it.
- name: Refund
run_when_succeeded: [Approval]
http:
url: a|var::public_base|/mock/payments/refund
method: POST
headers:
content-type: application/json
body: '{"order_id": "a|body::order_id|", "amount": a|body::amount|, "approved_by": "a|Approval::approved_by|"}'

# ── Stand-in for a payment provider, so the pack runs on its own ──────────
mock/payments/refund:
output: http
method: POST
summary: Stand-in payment provider
tags: [demo]
actions:
- name: Charge
input: a|body|
json_output: '{}'
hide_data_on_success: true
response_on_success:
http_code: 200
headers:
content-type: application/json
body: '{"refunded": true, "order_id": "a|Charge::order_id|"}'

demo.yml

name: HumanApprovalDemo

docs: true

# The same approval flow with the email step removed, so the pack can be run with no SMTP.
# Instead of sending the link, this returns it — which is also the shape you want when the
# approval lives in your own UI rather than an inbox.

interfaces:

demo/refunds/request:
output: http
method: POST
summary: Request a refund and get back the link that approves it
tags: [demo]
request_example: '{"order_id": "A-1001", "amount": 4200}'

actions:
- name: Ticket
json_output: '{"token": "a|uuid|"}'

# Where a real config emails the link, this records it. The run parks either way, so the
# link has to be built BEFORE the wait — which is why the token is minted in its own
# action rather than by the wait itself.
- name: Link
run_when_succeeded: [Ticket]
json_output: '{"approve_url": "/_ap/resume/a|org_uuid|/a|Ticket::token|"}'

- name: Approval
run_when_succeeded: [Link]
wait_for_callback:
token: a|Ticket::token|
timeout: "1h"

- name: Refund
run_when_succeeded: [Approval]
database: main
query: INSERT INTO ap_approval_demo (order_id, approved_by) VALUES ($1, $2)
params:
- a|body::order_id|
- a|Approval::approved_by|

global:
databases:
main:
driver: postgres
conn_string: "a|ap_var::DATABASE_URL|"