AFP engineering series · Chapter 13 of 25
Compare middleware screening with durable workflow interrupts, and prevent replay or stale approval from authorizing effects.
Two integration problems need two mechanisms
Content screening belongs near the data entering or leaving a model. A human approval may require a workflow to pause for minutes or hours and resume after a process restart. Treating both as a single synchronous callback makes the application difficult to recover and audit.
LangChain documents deterministic and model-based guardrails through middleware. LangGraph documents interrupts that persist workflow state and resume with external input. These provide integration mechanisms. The action authorization, approval binding, and transaction design below remain application responsibilities.
Use middleware for bounded checks
A middleware layer can validate a request shape, redact selected fields, or call a classifier before generation. Keep its contract explicit: what it inspects, whether it can modify data, what it returns on denial, and how errors propagate. A successful transformation should preserve enough provenance to explain what changed.
Avoid using a text redactor as an authorization system. Removing an account number from the output does not prevent a tool from reading an unauthorized account. Likewise, a classifier that labels a request benign does not prove that the caller owns the referenced resource. Keep those checks in the resource service.
Use an interrupt for a durable decision point
A payment proposal can move into awaiting_review state and expose an approval payload. The payload should contain a server-owned proposal identifier and a human-readable view of the exact action. Authenticate the reviewer in the application that handles the decision, then resume the workflow with a reference to the protected decision record.
LangGraph's documentation requires a checkpointer and thread identifier for interrupt/resume workflows and warns about re-execution of code around interrupts. Treat code before the pause as potentially repeated. Do not send a payment or an email immediately before an interrupt and assume it can only happen once.
Separate preparation from execution
Illustrative python; adapt the persistence and identity adapters to your service.
# Framework-neutral state machine for a durable approval workflow.
def next_step(case):
if case.status == "draft":
return "validate_and_store_proposal"
if case.status == "awaiting_review":
return "pause"
if case.status == "approved":
return "revalidate_then_execute_idempotently"
if case.status == "uncertain":
return "reconcile_processor_status"
return "show_authoritative_result"Persist the proposal and its version before asking for approval. On resumption, reload current state rather than trusting a large client-supplied payload. Verify that the decision references the same proposal digest and that the reviewer remains authorized. The thread identifier is a cursor into workflow state, not a bearer credential granting access to it.
Protect resume endpoints with the same tenant and principal checks as ordinary application endpoints. A user who guesses another thread identifier must not be able to approve or inspect its pending action. Include cross-tenant resume attempts in the integration test suite.
Test failure windows explicitly
| Failure window | Risk | Required behavior |
|---|---|---|
| After proposal stored, before prompt shown | Orphaned pending record | Recover or expire safely |
| After approval recorded, before resume | Decision lost to worker | Resume from durable record |
| After processor accepted, before result stored | Duplicate side effect on retry | Reconcile with operation identity |
| After policy changed, before execution | Stale authorization | Revalidate current policy |
A crash test is more useful than a happy-path demonstration here. Stop the worker at each boundary, restart it, and verify the final ledger and audit trail. Use a fake external processor that can accept a request and then simulate a network timeout. That reproduces the uncertain outcome that often causes duplicate effects.
Also test a reviewer changing their mind before execution. Define whether revocation is supported, how it races with the execution transition, and what the UI reports if the action has already committed. These are product semantics, not details the framework can infer.
Combine middleware and workflow policy coherently
Version the content policy, workflow state schema, and action policy separately. A resumed workflow may have been created under an older schema. Migrate it deliberately or route it for review; do not deserialize it into new code and assume every field has the same meaning.
Keep a shared decision vocabulary across middleware and workflow nodes: allow, deny, review, unavailable, and invalid. Preserve the distinction between a blocked response and a blocked action. A workflow can successfully prevent a payment while still needing to correct an unsafe generated explanation.
Worked crash recovery: approval resumes after processor acceptance
A workflow stores an approved refund proposal, submits it to a sandbox processor, and crashes before storing the response. On restart, the graph resumes from a checkpoint before the external call. Without an operation identity and reconciliation rule, the same node can submit a second refund. Durable workflow state alone does not make an external side effect exactly once.
Create the operation record before submission and use its identifier consistently at the processor. Persist the reserved balance and proposal digest. On resumption, inspect the operation state. If the outcome is uncertain, query or reconcile the existing processor operation rather than creating a new one. The exact strategy depends on the provider's idempotency and lookup contract.
Test three crash points: before submission, after processor acceptance, and after local completion. A fake processor should record accepted operations even when it returns a timeout. Assert that all recovery paths converge to one logical effect and a consistent ledger. The graph's final message should reflect the authoritative operation state, not simply the absence of an exception.
Protect the resume endpoint. A thread identifier locates state, but the caller still needs permission to view or resume it. Test another tenant attempting to submit an approval to the same identifier. Validate the approval record against the stored proposal version rather than accepting arbitrary resume fields as trusted facts.
Finally, upgrade the workflow schema while one case is paused. Define whether it resumes under the old compatible code, migrates explicitly, or requires renewed review. This is a real deployment concern for long-lived approvals. A framework comparison should include these recovery and migration semantics, because they determine whether the same security contract survives beyond a single happy-path execution.
Choose the framework from the workflow, not the brand
ADK callbacks and plugins can fit an agent-centric application; LangChain middleware can fit a model-call pipeline; LangGraph's durable execution can fit long-lived approval flows. None removes the need for backend permissions, idempotency, or evidence-based review. Compare the control points and recovery semantics your product needs.
Before migration, run the same synthetic cases against both implementations: unauthorized resource, changed proposal, duplicate resume, detector timeout, and legitimate approval. Compare final effects and audit records, not only response text. A framework migration is successful when the security contract survives the change in orchestration.
Practice and continue
Open the interactive companion lab to inspect the assumptions and outcomes with synthetic data. Then return to the 25-chapter AFP learning path.
Sources and implementation scope
Primary references checked October 15, 2026. Framework details follow the linked documentation; the Northstar cases, policy contracts, and test designs are original illustrative designs. Python snippets labeled illustrative are not complete production integrations. ADK package metadata checked for this series reported 2.12.0; pin and test the version used by your application.