Interceptors

Contents

  1. Workflow-Level Interceptors
  2. Defining Interceptors
  3. Interceptor Fields
  4. Scope Options
  5. {:tag :code, :attrs nil, :content [":all"]}
  6. {:tag :code, :attrs nil, :content ["{:id-match \"pattern\"}"]}
  7. {:tag :code, :attrs nil, :content ["{:cells [...]}"]}
  8. Execution Order
  9. How It Works
  10. Interceptors vs Maestro FSM Interceptors
  11. Common Use Cases
  12. Injecting navigation context for UI cells
  13. Request timing
  14. Error context enrichment

Workflow-Level Interceptors

Interceptors let you inject cross-cutting concerns (logging, context enrichment, error wrapping) into cell handlers without modifying the handlers themselves.

Defining Interceptors

Interceptors are declared in the workflow definition under :interceptors:

{:interceptors
 [{:id    :nav-context
   :scope {:id-match "ui/*"}
   :pre   (fn [data]
            (if (:user-id data)
              (assoc data :logged-in true :nav-items ["Dashboard" "Profile" "Logout"])
              (assoc data :logged-in false :nav-items ["Login" "Register"])))}

  {:id    :request-logging
   :scope :all
   :post  (fn [data]
            (println "Cell completed, keys:" (keys data))
            data)}]

 :cells {...}
 :edges {...}}

Interceptor Fields

FieldRequiredDescription
:idyesKeyword identifier for the interceptor
:scopeyesWhich cells this interceptor applies to (see Scope Options)
:preno(fn [data] -> data) — transforms input data before handler
:postno(fn [data] -> data) — transforms output data after handler

At least one of :pre or :post must be present.

Scope Options

:all — every cell in the workflow

{:scope :all}

{:id-match "pattern"} — glob pattern on cell :id

{:scope {:id-match "ui/*"}}       ;; matches :ui/render-dashboard, :ui/render-error
{:scope {:id-match "auth/*"}}     ;; matches :auth/validate-session
{:scope {:id-match "*"}}          ;; same as :all

The pattern is matched against the full namespace/name of the cell's :id.

{:cells [...]} — explicit cell name list

{:scope {:cells [:render-dashboard :render-error]}}

Cell names are the workflow-level names (keys in :cells map), not the cell registry IDs.

Execution Order

Interceptors compose in declaration order:

:interceptors [{:id :first, :scope :all, :pre pre-1, :post post-1}
               {:id :second, :scope :all, :pre pre-2, :post post-2}]

Execution: pre-1 → pre-2 → handler → post-1 → post-2

Both :pre and :post functions run in declaration order (first declared runs first).

How It Works

At compile-workflow time, for each cell that matches any interceptor's scope, the cell's handler is wrapped:

original-handler
  ↓ wrapped by matching interceptors
intercepted-handler = (fn [resources data]
                        (->> data
                             (apply-pre-interceptors)
                             (original-handler resources)
                             (apply-post-interceptors)))

This happens before the FSM is built — the interceptor wrapping is transparent to Maestro.

Interceptors vs Maestro FSM Interceptors

Workflow-Level InterceptorsMaestro FSM Interceptors
Signature(fn [data] -> data)(fn [fsm-state resources] -> fsm-state)
ScopePer-cell, pattern-matchedGlobal (all states)
Defined inWorkflow :interceptorsrun-workflow opts :pre/:post
PurposeCell-specific data transformsSchema validation, tracing

Both can coexist. Schema interceptors (pre/post) run at the FSM level and are always present. Workflow interceptors wrap individual cell handlers inside the FSM state.

Common Use Cases

Injecting navigation context for UI cells

{:id    :nav-context
 :scope {:id-match "ui/*"}
 :pre   (fn [data]
          (assoc data
                 :logged-in (boolean (:user-id data))
                 :nav-items (if (:user-id data)
                              ["Dashboard" "Profile" "Logout"]
                              ["Login"])))}

Request timing

{:id    :timing
 :scope :all
 :pre   (fn [data] (assoc data ::start-ns (System/nanoTime)))
 :post  (fn [data]
          (let [elapsed (/ (- (System/nanoTime) (::start-ns data)) 1e6)]
            (println "Cell took" elapsed "ms")
            (dissoc data ::start-ns)))}

Error context enrichment

{:id    :error-context
 :scope :all
 :post  (fn [data]
          (if (:error-type data)
            (assoc data :error-context {:timestamp (java.time.Instant/now)
                                         :workflow-id :my-workflow})
            data))}