Advanced Multi-Feature Flow
Complex production example combining webhook ingestion, templates, email notifications, human approvals, retries, and persistence.
Quick Answer
Use this pattern when you need one flow to process webhook events, send templated email notifications, gate with approval, and persist artifacts/audit state.
Use Case
Order risk workflow with:
- webhook/manual interfaces + top-level schedule
- template-driven HTTP/notification payloads
- human approval wait
- email notifications via SendGrid HTTP channel
- persisted result artifacts (
res://) - KV audit write for cross-run lookup
Full Definition
{:slug :advanced-order-orchestration
:name "Advanced Order Orchestration"
:concurrency {:type :keyed
:key-field :order-id
:on-new-version :drain}
:requires [{:slot :orders-api
:type :http-api
:label "Orders API"
:base-url "https://api.example.com"
:auth {:type :bearer}}
{:slot :sendgrid
:type :http-api
:label "SendGrid API"
:base-url "https://api.sendgrid.com"
:auth {:type :bearer}}
{:slot :webhook-signing-secret
:type :secret
:label "Webhook Signing Secret"}]
:templates [{:id :fetch-risk
:type :http-request
:request {:method :get
:path "/orders/{{order-id}}/risk"}}
{:id :approval-notice
:type :notification
:channels {:http {:connection :sendgrid
:path "/v3/mail/send"
:method :post
:json {:personalizations [{:to [{:email "approver@example.com"}]}]
:from {:email "approvals@example.com"}
:subject "Approve order {{order-id}}"
:content [{:type "text/plain"
:value "Approve: {{approval-url}}\nReject: {{rejection-url}}"}]}}}}]
:functions [{:id :risk-summary
:language :clojure
:code "(fn [risk] {:risk-level (:risk-level risk) :risk-score (:score risk) :needs-approval (= :high (:risk-level risk))})"}]
:invocations {:default
{:inputs [{:name :order-id
:type :text
:label "Order ID"
:required true}]}}
:interfaces {:manual [{:id :run :label "Run" :invocation :default :enabled true}]
:webhook [{:id :order-updated
:invocation :default
:event-name "orders.updated"
:auth {:type :hmac-sha256
:header "Stripe-Signature"
:secret-ref :webhook-signing-secret}}]}
:schedules [{:id :hourly
:label "Hourly"
:invocation :default
:enabled true
:cron "0 * * * *"}]
:flow
'(let [input (flow/input)
order-id (:order-id input)
order (flow/step :http :fetch-order
{:connection :orders-api
:method :get
:path (str "/orders/" order-id)
:retry {:max-attempts 3 :backoff-ms 500}})
risk (flow/step :http :fetch-risk
{:connection :orders-api
:template :fetch-risk
:data {:order-id order-id}
:retry {:max-attempts 3 :backoff-ms 1000}})
summary (flow/step :function :risk-summary
{:ref :risk-summary
:input risk})
risk-artifact (flow/step :function :persist-risk
{:input {:order order :risk risk :summary summary}
:persist {:type :blob}})
approval ^{:label "Manager approval required?"
:yes "Wait for manager action"
:no "Auto-approve"}
(if (:needs-approval summary)
(flow/step :wait :manager-approval
{:key (str "approval:order:" order-id)
:timeout "2h"
:on-timeout :continue
:default-value {:action :reject :reason :timeout}
:notify {:template :approval-notice
:data {:order-id order-id
:risk-score (:risk-score summary)}}})
{:action :approve :reason :auto})
action (keyword (or (:action approval) "reject"))
fulfillment ^{:label "Fulfillment decision"
:yes "Create fulfillment"
:no "Skip fulfillment"}
(if (= :approve action)
(flow/step :http :fulfill-order
{:connection :orders-api
:method :post
:path (str "/orders/" order-id "/fulfill")
:retry {:max-attempts 4 :backoff-ms 1500}})
{:status :not-fulfilled})
_audit (flow/step :kv :write-audit
{:operation :set
:key (str "orders:audit:" order-id)
:value {:order-id order-id
:action action
:risk-level (:risk-level summary)
:artifact-ref (:uri risk-artifact)}
:ttl 604800})
notify (flow/step :notify :send-outcome-email
{:channels {:http {:connection :sendgrid
:path "/v3/mail/send"
:method :post
:json {:personalizations [{:to [{:email "ops@example.com"}]}]
:from {:email "alerts@example.com"}
:subject "Order {{order-id}} result"
:content [{:type "text/plain"
:value "action={{action}}, artifact={{artifact-ref}}"}]}}}
:data {:order-id order-id
:action (name action)
:artifact-ref (:uri risk-artifact)}})]
{:order-id order-id
:action action
:approval approval
:risk-summary summary
:risk-artifact-ref (:uri risk-artifact)
:fulfillment fulfillment
:notification notify})}
Explanation Highlights
- Webhook ingress is modeled as
:interfaces :webhookwith explicit auth. - Risk payload is persisted as
res://to avoid large inline state. - Wait step uses canonical
:key+:timeout+:on-timeoutsemantics. - Branch metadata labels (
^{:label ...}with:yes/:no) make decisions readable in run timelines. - Approval notification template drives email-like HTTP channel delivery with action links.
- Notify step sends final outcome email through SendGrid HTTP API.
- KV write stores compact audit state for later lookup.
Operational Notes
- keep webhook auth on; avoid unauthenticated public endpoints
- keep wait timeouts bounded and branch timeout behavior explicitly
- treat
:httpchannel as the canonical email delivery path - watch result sizes: persist early when output may exceed the 512 KB inline
result threshold; 1 MB is the hard step/DB/code/notify payload cap, while
persisted blob writes cap at 50 MB retained or 4 GB ephemeral