Docs
Examples

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

  1. Webhook ingress is modeled as :interfaces :webhook with explicit auth.
  2. Risk payload is persisted as res:// to avoid large inline state.
  3. Wait step uses canonical :key + :timeout + :on-timeout semantics.
  4. Branch metadata labels (^{:label ...} with :yes/:no) make decisions readable in run timelines.
  5. Approval notification template drives email-like HTTP channel delivery with action links.
  6. Notify step sends final outcome email through SendGrid HTTP API.
  7. 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 :http channel 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

Related

As of May 20, 2026