A user approves an agent's proposed interpretation of two data files. Before the investigation continues, one of those files is replaced.

Should the earlier approval still count?

That question became important while we were building a reconciliation reference implementation at Cloudnaut using Strands Agents and Amazon Bedrock. We needed the application to preserve exactly what the user had reviewed, detect changes, and resume only when the confirmation still applied.

The implementation brought together three responsibilities: the model's proposal, the user's decision, and the application's enforcement of that decision.

The workflow we built

Our application works with uploaded orders and captured-payment CSVs. It identifies differences, reports exceptions, and links findings to supporting records.

The responsibilities are deliberately separated:

ComponentResponsibility
Model through Amazon BedrockSelect bounded investigation tools and propose mappings when confirmation is needed
Strands Agents SDKRun the interaction between the model and its tools, with hooks for application controls
Python and DuckDBValidate records, apply reconciliation rules, and calculate amounts using integer paise
Application state and APIPersist investigations, capture approval requests, verify confirmations, and reconstruct the confirmed context
Answer renderingPresent verified figures, findings, caveats, and evidence references

Strands provides the agent loop: the model can request a tool, receive its result, and decide what to do next.1 We expose a small set of tools for dataset inspection, mapping validation, reconciliation, evidence retrieval, and policy explanation.

The approval boundary sits around the interpretation the workflow will use. For example, an orders export may call its amount column amount, while the application's canonical field is order_amount. A candidate mapping connects the two.

The application can check that the column exists and that the required fields are covered. The user still needs to confirm that this is the intended interpretation of the supplied export.

A valid proposal can still require approval

During live validation, we encountered a useful distinction.

The model supplied a structurally valid mapping proposal. Validation succeeded. However, the caller had not yet confirmed it, so the reconciliation tool correctly refused to return an authorised reconciliation result.

Our pause handling initially recognised mapping ambiguity, but did not consistently recognise a valid proposal awaiting approval. The investigation could therefore end in a failure state instead of presenting a clean confirmation request.

We corrected the transition by handling three outcomes explicitly:

  • The proposed mapping needs clarification.
  • The proposed mapping is valid and requires caller confirmation.
  • Reconciliation was requested while the mapping remained unconfirmed.

Each outcome pauses the investigation with an actionable request. The response identifies what the user must review. Reconciliation figures are withheld from that paused response.

This distinction matters whenever validation and authorisation answer different questions. A tool argument may satisfy its schema while still requiring a person's decision before the workflow can use it.

Give approval a precise scope

A confirmation needs to identify the operation and inputs it covers.

For our mapping-confirmation flow, the saved approval request carries:

FieldWhat it identifies
Case IDThe investigation's case
RevisionThe version of the case the user reviewed
Orders and payments hashesThe contents of the two input files
CutoffThe time boundary used by the investigation
Candidate mappingThe exact proposed interpretation of the columns

We capture this snapshot when the application pauses and persist it. A later request for the paused investigation returns that saved snapshot.

That last detail is important. If the case advances to a new revision, fetching the old investigation must not quietly replace its approval identity with the newest inputs. The user needs to see the request that was actually pending.

When confirmation arrives, the application compares three mappings:

  1. The candidate saved with the pause.
  2. The candidate named in the submitted approval.
  3. The mapping the caller is now asking the application to bind.

All three must match. The input identity must also still match the case being resumed.

Otherwise, an application could accept approval for mapping P and subsequently bind a different, structurally valid mapping Q.

The server verifies these relationships against its own saved state. Returning an approval object to the server is insufficient on its own. These consistency checks also sit alongside the authentication and access-control requirements of any environment in which the pattern is deployed.

Persist the pause and reconstruct the confirmed context

Our implementation uses an application-owned pause and resume lifecycle around a real Strands agent. Investigation state is persisted in SQLite, with uploaded bytes held by the local blob adapter.

The Strands SDK also documents hooks and interrupts for human input.2 The application still needs to define what approval covers and verify that scope when execution resumes.

The lifecycle looks like this:

Approval workflow showing the persisted request, confirmation checks, refusal path, and resumed execution

View / edit diagram source ↗

In our Strands integration, an after-tools hook recognises the structured confirmation outcomes and ends the current turn. The application then records the pending request.

On confirmation, the application reads the inputs, verifies their identity and the submitted mapping, binds the confirmed context, and runs the resumed phase. The check happens before binding or dispatching that phase.

The core guard can be expressed with the following simplified application logic. These helper names are illustrative, rather than Strands SDK APIs:

def resume_investigation(saved_pause, current_case, confirmation):
    if saved_pause.consumed:
        raise ConfirmationRefused("This pause has already been consumed")

    if confirmation.authorized_for != saved_pause.authorization_identity:
        raise ConfirmationRefused("Approval does not match the saved request")

    if current_case.input_identity != saved_pause.input_identity:
        raise ConfirmationRefused("The investigation inputs have changed")

    if confirmation.mapping != saved_pause.candidate_mapping:
        raise ConfirmationRefused("The requested mapping was not approved")

    context = bind_confirmed_mapping(current_case, confirmation.mapping)
    outcome = run_strands_phase(context)
    persist_outcome_and_consume_pause(saved_pause, outcome)
    return outcome

The resulting outcome is saved under the original public investigation ID. A later status request retrieves that saved outcome, including after an application restart.

After the resume outcome has been saved and the pause consumed, another confirmation is refused. A page refresh retrieves state through a read request; it does not automatically resubmit approval or rerun the investigation.

Test the transitions around approval

A successful approval is one part of the behaviour. We also need to know what happens when the surrounding context changes.

Our offline API and runner tests exercise these cases:

ScenarioRequired behaviour
Valid proposal without confirmationPause with a specific approval request and no reconciliation figures
Confirmation without its approval identityRefuse before binding or resume
Approved candidate and requested mapping differRefuse, even when both mappings are structurally valid
Inputs or cutoff change after the pauseReject the stale confirmation
Application restartsRecover saved state; completed results remain retrievable
Confirmation is repeated after completionRefuse the consumed pause without rerunning it

We also check execution traces. Prior tool results can remain available as context, but replaying them must not make them appear as newly executed tools.

For the controlled mapping-confirmation scenario, the recorded offline trace contains two primary executions, dataset inspection and mapping validation, followed by one new reconciliation execution on resume.

Separate live validation exercised the confirmation workflow through Strands and Amazon Nova 2 Lite on Bedrock. A browser-driven check used the alternative amount-column header, accepted scoped confirmation, and returned the expected complete result.

The underlying cases use synthetic data with known expected outcomes. The offline tests cover the surrounding transitions; the recorded live checks establish that representative scenarios also work with actual model calls.

Apply the pattern to the decision your user is making

Mapping approval is a useful example because a small interpretation change can alter every subsequent result. The same design question arises when a person reviews a record selection, an operational action, or a proposed change to a business rule.

The approval object should describe that decision at the level execution will use. For a data investigation, that includes the inputs and their interpretation. For an action in another system, it may include the target resource and the exact parameters to apply.

Before implementing an approval screen, decide what invalidates the approval, what must survive a restart, and how the execution path checks both. Those decisions determine what the application has to persist and enforce when the agent continues.

Footnotes

  1. Strands Agents documentation: Anatomy of an agent. ↩
  2. Strands Agents documentation: Interrupts. ↩