Exam Room · Advanced GenAI

How to Wire an LLM to Side-Effecting Actions with Bedrock AgentCore

June 22, 2026 · 25 min read

Generative AI Development · part of The Exam Room

The situation

Support engineering has a next-step list for the assistant that currently only answers questions. The new asks are actions:

  • Look up a customer’s subscription by email or ID. Hits an internal subscriptions API.
  • Pause or resume a subscription. Same API, different endpoint, side-effecting.
  • Issue a refund for a specific charge. Hits the billing service; writes to the ledger.
  • Send a confirmation email after any action. Hits the notifications service.

Four tools. Each lives behind an internal HTTPS API with OAuth2 client credentials. Each has a JSON schema. Each has a blast radius: the lookup is safe; the pause is reversible; the refund is money changing hands. The assistant has to know which tool to call, pass the correct arguments, handle errors, and stop and confirm with the user before anything with a blast radius runs.

The team has six weeks and two engineers. They already have a working retrieval assistant from the previous iteration.

What actually matters

An agent loop has the same shape wherever it’s implemented. The model is given a set of tools, each with a name, description, and input schema. The user asks something. The model decides whether to call a tool, and if so, which one and with what arguments. The caller (us, or a framework, or Bedrock) invokes the tool, gets a result, feeds it back to the model. The model either calls another tool, asks the user a question, or produces a final answer. Repeat until done.

That framing exposes the decisions. The first is tool definition: how tools are described to the model, and how tightly their schemas are enforced. The second is invocation: when the model says “call tool X with arguments Y,” who actually executes that? A Lambda? A local Python function? A remote service? The third is error recovery: when a tool fails, does the agent see the error and retry, or does the whole conversation crash? The fourth is confirmation and guardrails: how the system stops before a side-effecting action and asks the user, and how we prevent the model from calling refund when the user said pause. The fifth is observability: traces of which tools ran, with what inputs, what outputs, how long, how much. The sixth is session state: conversation history and intermediate tool results need to persist across turns without ballooning the PromptThe input you hand to an LLM – system instructions, user message, examples, retrieved documents, tool descriptions, the lot..

Another thing worth thinking about is where the danger lives. The language model is non-deterministic. A tool that sends money can’t be called non-deterministically. The architecture has to make it structurally impossible for the model to skip a confirmation step for a side-effecting action, not because the prompt told it not to, but because the code won’t let it.

Then there’s debuggability in production. When an agent does the wrong thing, calls the wrong tool, passes the wrong arguments, confuses two customer IDs, we need to see the model’s reasoning, the tool inputs and outputs, the retry attempts, and the final user response. Not just at dev time; every production invocation.

What we’ll filter on

  1. Tool-definition overhead, how much glue code per tool we write?
  2. Side-effect safety, is confirmation a structural guarantee or a prompt hope?
  3. Observability out of the box, traces, metrics, replays without building it ourselves?
  4. Flexibility, custom tool-selection logic, custom reasoning loops, dynamic tool sets?
  5. Deployment shape, managed service, container, Lambda, all of the above?

The landscape

  1. Bedrock AgentCore. The operational building blocks for running an agent you wrote yourself. A serverless runtime executes the agent code with per-session isolation; a gateway exposes existing APIs and Lambda functions as tools through one uniform interface, so the four internal endpoints become callable without being rewritten as bespoke agent actions; an identity capability brokers scoped credentials so the agent reaches the subscriptions and billing APIs without a standing key in the code; observability emits traces of every step, tool call, and result. The reasoning loop is ours, which means the confirmation gate is ours to build and ours to guarantee. Framework-agnostic and model-agnostic. Ticks 1, 3, 4, 5; 2 becomes a property of code we write rather than a checkbox.

  2. Bedrock Agents Classic. The predecessor, where AWS orchestrated the loop and requireConfirmation on an action group made the gate a config property. It moved to maintenance in June 2026 and closed to new customers at the end of July; existing agents keep running. A team already holding one has a working answer and should read this for the gate design rather than the wiring. For a build starting now it is not selectable, so it stays out of the comparison below.

  3. A framework agent on our own infrastructure. LangChain or LangGraph, deployed to Lambda, Fargate, or EKS that we operate. LangChain defines tools as Python callables with type-annotated arguments; LangGraph models the agent as a directed graph of nodes. The loop is explicit code we own, exactly as on AgentCore, but session isolation, credential brokering, and tracing come with it as work rather than as services. LangSmith gives traces for a separate subscription. Ticks 4 cleanly; 1, 3, and 5 become ours to build.

  4. Custom tool router. We write the loop ourselves against a foundation model’s native tool-use API. Claude’s tools parameter, Nova’s equivalent, invoking tools with whatever runtime we like. The model returns a structured tool_use block; we parse it, run the tool, feed the result back as a tool_result block, repeat until the model returns plain text. Maximum control, maximum code, and every operational concern is ours. Ticks 4 entirely; gives up 1 and 3.

  5. Step Functions + Bedrock. Not an agent, strictly. Step Functions as the orchestrator, Bedrock as a step, tool invocations as other steps. Works when the flow is largely deterministic with a language-model step in the middle: classify the request, then follow a hand-drawn state machine. It does not handle free-form multi-turn reasoning. Useful shape for certain problems; wrong shape for an open-ended support assistant.

  6. The older chain pattern. A fixed sequence of model calls. Worth naming to rule out: when the control flow is hard-coded it is not really an agent, and the support assistant’s branching is wide enough that a chain would become a mess of if-statements.

Evaluation

Side by side

Option Tool-def overhead Side-effect safety Observability Flexibility Deployment
Bedrock AgentCore Existing APIs via gateway Our loop, our gate Traces built in High Managed serverless runtime
Framework on our infra Typed Python function Our loop, our gate LangSmith (separate) High Lambda / Fargate / EKS we run
Custom tool router Schema + dispatch code Our loop, our gate Whatever we build Total Anything
Step Functions + Bedrock State-machine steps Explicit states Native Low (not free-form) Managed

Reading it against the situation: side-effect safety is non-negotiable because a refund moves money, observability is non-negotiable because agent mistakes cost customer trust, and the flow is open-ended enough that Step Functions is the wrong shape. That leaves three options which all put the gate in code we write, so the question stops being who guarantees the confirmation and becomes how much operational machinery do we build around the loop. With two engineers and six weeks, the answer is as little as possible. AgentCore supplies the runtime, the tool interface, the credential brokering, and the traces, and leaves us the part that actually needs our judgement: the dispatcher that refuses to execute a side-effecting tool without a confirmation token.

The three agent loops, laid out

Bedrock AgentCore our loop, managed runtime and gateway LangChain / LangGraph framework loop, our code Custom tool router Claude tools API direct User message User message User message AgentCore Runtime our reason → plan → act loop session-isolated compute LangGraph agent node reason, route to tool, observe our Python in Lambda / Fargate Claude Messages API tools param · tool_use reply our dispatch code parses blocks Confirmation gate dispatcher withholds the call structural (token required) Confirmation gate if tool is side-effecting → pause we write the if-statement Confirmation gate every side-effecting branch we write all of it AgentCore Gateway existing APIs and Lambdas one uniform tool interface @tool Python function HTTP call to internal service we write the wrappers Dispatcher function switch(tool_name) → run every tool by hand Observation Lambda result → agent runtime Observation tool return value → next node Observation tool_result block → next call CloudWatch trace reasoning + tool I/O per step built in, no extra SaaS LangSmith trace node-by-node, external SaaS separate account & bill Whatever we build CloudWatch structured logs all of it ours
Same loop, three places to draw the line. Bedrock's orange boxes are managed; everything blue and purple is ours.

The solution

Tools through the gateway. The four internal endpoints already exist behind OAuth2 client credentials, and they stay as they are. AgentCore’s gateway exposes them through one uniform tool interface, so the agent discovers and calls subscriptions.lookup, subscriptions.pause, billing.refund, and notifications.sendEmail without any of them being rewritten as bespoke agent actions. The schema each tool advertises is what the model reads when deciding which to call, so the descriptions get the same care a public API reference would: what the tool does, what each argument means, and what it returns.

The confirmation gate, in our dispatcher. Agents Classic did this with configuration, a requireConfirmation flag on an action group. AgentCore has no such flag, so the gate is code we write, and it has to be as hard to bypass as the flag was. The tool dispatcher carries a table of which tools are side-effecting. When the model asks for one, the dispatcher does not call it. It returns a pending-confirmation result to the application, which surfaces the prompt to the customer, and the tool executes only when the reply arrives carrying a confirmation token the dispatcher itself issued. The model never sees the token and cannot mint one. subscriptions.lookup is read-only and runs straight through; pause, resume, and refund cannot execute without a token, whatever the model says. That gate is a branch in our code, which makes it testable: a unit test that asks the dispatcher to refund without a token and asserts it refuses is the single most valuable test in the codebase.

Identity, scoped and brokered. AgentCore’s identity capability issues scoped credentials for the calls the agent makes, instead of a standing key embedded in the code. The authenticated customer is the other half: their ID comes from the session the application established, never from an argument the model supplied. A model that has been talked into passing userId: "u_999" gets a refund attempt against its own session identity and fails, because the dispatcher reads the identity from the session context and ignores the parameter.

Isolation, memory, and traces. The runtime executes each conversation in its own session context, so one customer’s run shares nothing with another’s. Managed memory keeps the conversation coherent across turns and reconnects without us designing a datastore and a retention policy. Observability emits a trace per run: which steps executed, which tool was called with which arguments, what came back, how long it took, and where a run failed. When a customer says the assistant did the wrong thing, that trace is the answer.

Guardrails. Bedrock Guardrails apply at invocation, on both the input and the output: denied topics, PII redaction in logs, toxicity filtering. Blocked content returns a configured message instead of reaching a tool.

Worked example

Customer: “I want to cancel my subscription and get a refund for the last month.”

  1. The application starts a session, having already authenticated the customer as u_123. The agent sees the message, the gateway’s tool list, and the System promptThe instruction block that frames the model’s behaviour for a session, separate from the user’s messages..
  2. The model reasons that it needs to find the subscription, pause it, find the recent charge, and refund it. It asks for subscriptions.lookup. The dispatcher checks its table, sees a read-only tool, and calls straight through with the session’s identity. The subscription comes back.
  3. The model asks for subscriptions.pause. Side-effecting. The dispatcher returns a pending-confirmation result instead of calling anything, and the front-end renders “Confirm pausing subscription sub_xyz?” as a button.
  4. The customer confirms. The reply carries the dispatcher’s token; the call runs; the subscription is paused.
  5. The model asks for billing.refund on the last charge, $49. Pending again: “Confirm refunding $49 of charge ch_abc?”
  6. The customer confirms. The refund runs and the ledger is written.
  7. The model asks for notifications.sendEmail. Low blast radius, self-service, runs without a gate.
  8. The model produces a final response: the subscription is paused, $49 refunded, confirmation email sent.

The trace has eight entries. The session can be replayed. The refund could not have happened without two tokens the model had no way to produce.

What’s worth remembering

  1. An agent is a loop: reason, call tool, observe, repeat. Every framework has one, so the work is defining tools well, gating side effects, and capturing traces.
  2. A confirmation gate belongs in the dispatcher, not the prompt. Side-effecting tools return pending instead of executing, and run only against a token your code issued and the model never sees.
  3. Authenticated identity comes from the session, never from a tool argument. Read the customer ID from session context and ignore whatever the model passed.
  4. AgentCore is the operational half, not the loop: runtime isolation, a gateway that makes existing APIs callable, brokered credentials, and traces. You keep the reasoning and the judgement.
  5. Agents Classic closed to new customers in July 2026. Teams already running it keep their requireConfirmation guarantee; a build starting now rebuilds that gate in code, and should test it.

The assistant ships with four tools, three of them gated, full traces, and a refund flow that needs the customer to press a button twice before money moves. The model can still ask for the wrong thing; the architecture stops that wrong thing from costing the company.

These posts are LLM-aided. Backbone, original writing, and structure by Craig. Research and editing by Craig + LLM. Proof-reading by Craig.