Human-in-the-Loop Approval for Bedrock Agents: Where the Gate Actually Lives in the Flow

A prompt that says "ask before sending" is not an approval gate.

A reliable human-in-the-loop approval for Bedrock agents requires persisting the pending action outside the model context before any side effect occurs. If your safety boundary depends on a prompt directive telling the model to ask for confirmation, the agent will eventually re-evaluate its plan on a retry or a new session turn and execute the action without human sign-off.

Production systems fail when engineers treat approvals as conversational turns instead of discrete state machines. When an Amazon Bedrock agent invokes an action group backed by an AWS Lambda function, that invocation is stateless, subject to execution timeouts, and driven by transient prompt context. According to the Amazon Bedrock documentation on action groups, tools can be defined using OpenAPI schemas and are fulfilled either by invoking an AWS Lambda function or by returning control to the application. Building a production-grade AI agent approval gate means pulling the gate out of the language model and anchoring it into an external, durable coordination layer.

The gate is a state machine, not a sentence in your system prompt

Instructing an agent in its prompt to request confirmation before sending an email or charging a card provides zero transactional safety. Large language models are non-deterministic reasoning engines. As highlighted in the OWASP Top 10 for LLM Applications guide on excessive agency, autonomous agents granted direct execution credentials frequently bypass intended boundaries during iterative execution loops or prompt ambiguity. When an agent enters a tool-calling loop, encounters a transient network error, or receives follow-up context from a user in a multi-turn conversation, it frequently assumes that permission was already granted. Worse, on an automatic Lambda retry, the agent re-derives intent from raw instructions and executes the destructive tool call directly.

A human-in-the-loop approval for Bedrock agents must be enforced as a structural state machine that operates across three explicit states:

  • Requested: The agent pauses the consequential action, serializes the exact execution parameters into an immutable evidence payload, writes a local pending marker, and parks the execution.
  • Decided: An authenticated human operator reviews the evidence and explicitly sets the state to approved or denied.
  • Consumed: The executing agent reads the approval decision and claims it using an atomic conditional write, ensuring the underlying action executes exactly once.

The consumed state is where naive implementations fail. In cloud architectures, distributed retries are a baseline reality. As outlined in the AWS Well-Architected Agentic AI Lens, non-idempotent operations executed across agent steps require strict fencing and deduplication mechanisms. If an approval token remains valid after its initial execution, a transient 504 Gateway Timeout between Bedrock and your action group Lambda will cause the Bedrock runtime to retry the call. The retried Lambda will see the record marked "approved" and execute the consequential action a second time. A gate must transition to consumed during the execution transaction itself.

This design also clarifies where developers make structural placement errors. Placing an approval check inside the business logic of an action group after calculations run is fragile, but placing it after external calls have begun is catastrophic. The gate must sit strictly between the agent's decision to act and the external side effect. The planning agent suggests an intent, but only a consumed approval unlocks the executor.

Mapping the Bedrock agent workflow: action groups, Lambda, and the session boundary

In a standard Bedrock agent workflow, the agent coordinates user inputs, historical session context, and OpenAPI schemas defining action groups. When Bedrock determines that a tool is needed, it structures an invocation payload conforming to your OpenAPI schema and sends it to the target Lambda function. The session state tracked by Bedrock's sessionId is ephemeral: it retains chat history within the configured idle session TTL, but it does not act as a transactional database.

If an action requires human review, the Lambda backing the action group must not sit idle waiting for a human to respond. Per the AWS Lambda quotas on execution timeouts, functions have a hard limit of 15 minutes, and Amazon Bedrock action group invocations will fail long before that due to gateway timeouts. Holding an open connection while waiting for a person to check their notifications causes immediate pipeline failure.

Instead, the workflow must be split across the session boundary using a decoupled check:

// 1. Compute deterministic hash of the action payload
const actionHash = crypto
  .createHash('sha256')
  .update(JSON.stringify(normalizedPayload))
  .digest('hex');

// 2. Evaluate approval state before executing side effect
const approval = await getApprovalRecord({
  agentId: event.agent.id,
  actionHash: actionHash,
  idempotencyKey: event.inputTextId
});

if (!approval || approval.status !== 'APPROVED') {
  // Park the workflow: request approval and yield execution
  await createApprovalRequest({
    summary: "Refund customer cust_88319",
    actionHash: actionHash,
    payload: normalizedPayload
  });

  return {
    messageVersion: "1.0",
    response: {
      actionGroup: event.actionGroup,
      apiPath: event.apiPath,
      httpMethod: event.httpMethod,
      httpStatusCode: 202,
      responseBody: {
        application_json: {
          status: "PENDING_APPROVAL",
          message: "Action requires human approval. Request has been submitted."
        }
      }
    }
  };
}

The conditional check hinges on the action_hash. Normalizing the parameters (such as sorting JSON keys and trimming strings) prevents parameter tampering. If the agent generates an intent to issue a refund to customer cust_88319, that specific tuple produces a unique hash. If the agent later attempts to execute an action with modified parameters, the hash check misses, and the execution is blocked. For teams managing external side effects, AgentDraft provides the ops API for AI agents: a per-agent email inbox, a conflict-free calendar, human approvals, and an audit trail behind one API.

What the approval request must carry to be auditable

A bare notification stating "Agent requests approval to run action_group_payments" is useless to a human reviewer and indefensible during an audit. The approval request must balance human readability with strict mechanical reproducibility.

To satisfy both needs, the approval request requires two core payloads:

  1. A plain-text summary: A single-line sentence that an operator can review quickly (e.g., "Cancel subscription and issue refund to org_7712").
  2. An immutable evidence payload: A structured JSON document containing the exact parameters the agent plans to feed the downstream API.

AgentDraft lets an agent pause any consequential action for human sign-off: it opens an approval request carrying a one-line summary and a JSON evidence payload, a person approves or denies it in the dashboard with an optional note, and the agent reads the outcome back. The gated action does not have to be one AgentDraft performs — a deploy, a migration, or a refund is gated the same way. Every transition lands in the append-only audit trail and fires an approval.* webhook.

Consider the differences across three typical tool contexts:

// Scenario A: Financial Refund
{
  "action_class": "billing.refund",
  "idempotency_key": "ref_9921_attempt_1",
  "evidence": {
    "customer_id": "cust_88319",
    "invoice_id": "in_2026_0911",
    "currency": "usd",
    "reason": "Agent triage: duplicate charge verified"
  }
}

// Scenario B: Calendar Hold Commit
{
  "action_class": "calendar.commit",
  "idempotency_key": "cal_hold_4410",
  "evidence": {
    "calendar_id": "primary",
    "hold_token": "hld_live_9031ba",
    "start_bucket": "2026-10-18T14:00:00Z",
    "duration_minutes": 30,
    "attendees": ["lead@acme.corp"]
  }
}

// Scenario C: Customer-Facing Email
{
  "action_class": "mailbox.send",
  "idempotency_key": "msg_send_3310",
  "evidence": {
    "to": "client@enterprise.com",
    "subject": "Updated Service Level Agreement",
    "body_sha256": "4b227777d4dd1fc61c6f884f48641d02b4d121d3fd328cb08b5531fcacdabf8a",
    "template_id": "sla_notice_v2"
  }
}

Crucially, the evidence payload must become read-only the microsecond it is recorded. If an agent could modify the payload after the human approves it, the entire audit chain is invalidated. Developers handling scheduled actions can read more about coordinating resources in our guide on calendar APIs for agents.

Deciding in the dashboard, not in an email link

A common anti-pattern in early agent architectures is embedding "Approve" and "Deny" links directly in email notifications sent to human managers. This pattern introduces severe vulnerabilities. Modern email security gateways, anti-phishing scanners, and mail client link pre-fetchers automatically issue HTTP GET requests to links inside inbound messages. If the link triggers an approval directly or via a single redirect, the automated scanner approves the agent's action before a human ever opens the message.

Furthermore, email is fundamentally unauthenticated at the link level once delivered. If a manager forwards an email containing an active approval link to a public channel or a teammate, anyone with access to that URL holds the ambient authority to execute the underlying system command.

Approvals are decided in the AgentDraft dashboard. AgentDraft emails the workspace owner a notification linking to the queue, but the decision itself is made signed in — there are deliberately no approve-from-email links, because an unauthenticated one-click approve is an attack surface. Slack, Discord, Teams, SMS and push delivery are not available today.

Human operators sign in to the AgentDraft dashboard using a passkey (WebAuthn), with an email magic link serving as the bootstrap and recovery path. Conforming to the W3C Web Authentication specification, this authentication model ensures cryptographic certainty regarding the reviewer's identity. The audit entry binds the decision to an authenticated user session rather than an anonymous network request. If your development team lives in chat applications, you must build an operational bridge that links teammates directly to the dashboard approval queue rather than attempting to execute ambient approvals directly within chat webhooks.

Reading the outcome back without double-executing

Once a human approves an action, the Bedrock agent needs to resume execution. There are two primary architectural patterns to facilitate this: periodic polling or asynchronous event notification via webhooks.

Polling introduces latency and wastes invocation cycles. The preferred pattern leverages event-driven execution using webhooks. Every transition lands in the append-only audit trail and fires an approval.* webhook, notifying your infrastructure the moment a human resolves a request. The downstream system can then re-invoke the Bedrock agent or trigger the executing worker directly.

However, webhook endpoints exposed to the public internet introduce attack vectors: spoofed payloads and replay attacks. An attacker capturing a signed "approved" payload might replay it minutes later to re-trigger an execution. To prevent this, every webhook delivery must be cryptographically verified and checked against an enforcement window.

AgentDraft signs every webhook delivery with an X-AgentDraft-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256> header computed over <t>. followed by the raw request body, using the workspace's signing secret. The receiver verifies it; AgentDraft recommends that receivers reject any timestamp more than 300 seconds (5 minutes) from the current time to block replays, and its documented reference verifier does so. Signed webhook delivery is available on every plan, including the free Developer tier.

For detailed verification code and implementation steps, consult our technical breakdown of signed webhooks for agent workflows. Below is an example verification check designed for an AWS Lambda function running behind Amazon API Gateway:

import crypto from 'crypto';

export async function handler(event) {
  const signatureHeader = event.headers['x-agentdraft-signature'] || event.headers['X-AgentDraft-Signature'];
  if (!signatureHeader) {
    return { statusCode: 401, body: 'Missing signature header' };
  }

  // Extract timestamp and signature components
  const parts = Object.fromEntries(
    signatureHeader.split(',').map(kv => kv.split('='))
  );
  const timestamp = parseInt(parts.t, 10);
  const signature = parts.v1;

  // Enforce 300-second replay window at the receiver
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - timestamp) > 300) {
    return { statusCode: 400, body: 'Webhook timestamp expired or skew too large' };
  }

  // Compute expected HMAC-SHA256 over "<timestamp>.<raw_body>"
  // Note: Raw unparsed string body must be used to preserve byte accuracy
  const payloadToSign = `${parts.t}.${event.body}`;
  const hmac = crypto.createHmac('sha256', process.env.AGENTDRAFT_WEBHOOK_SECRET);
  const digest = hmac.update(payloadToSign).digest('hex');

  const isValid = crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(digest, 'hex')
  );

  if (!isValid) {
    return { statusCode: 403, body: 'Invalid webhook signature' };
  }

  const payload = JSON.parse(event.body);
  
  // Guard against race conditions: verify local pending state exists
  await processDecidedApproval(payload);

  return { statusCode: 200, body: JSON.stringify({ received: true }) };
}

Notice the defensive sequencing: the receiver must enforce the 300-second window before computing the HMAC. Furthermore, your Lambda must inspect the raw, unparsed request body string directly from the event. If your framework parses JSON prior to running signature verification, key ordering or whitespace discrepancies will alter the byte stream and cause signature validation to fail.

A frequent failure mode occurs when an approval is granted rapidly, and the webhook fires before the requesting agent has finished persisting its internal state. To mitigate this race condition, the planning agent must commit its pending execution record to a durable store before dispatching the approval request over the network.

Scoping the agent's credentials so the gate cannot be bypassed

A gate provides no defense if the agent possesses the ambient credentials to execute the downstream action directly. If the planner agent holds administrative API tokens for payment gateways, message brokers, or calendar providers, a compromised LLM reasoning loop can easily bypass the gate by invoking the native tools or raw HTTP endpoints directly.

The solution is strict privilege separation between the planning agent and the execution worker. The agent interacting with the user should hold permissions only to inspect context and request approvals.

AgentDraft agents authenticate with bearer API keys prefixed avs_live_, stored argon2id-hashed. Each key carries explicit scopes (availability:read, bookings:read, bookings:write, mailbox:read, mailbox:write, rules:read, approvals:request); new keys default to availability:read and bookings:write, and approvals:request must be granted per key.

In a hardened Bedrock environment, your execution pipeline should look like this:

  • Bedrock Planning Lambda: Authenticates via an avs_live_ key scoped strictly to approvals:request and availability:read. It has no network route or credentials to write bookings, send external mail, or trigger financial mutations.
  • Dashboard Approver: An authenticated human signs in with WebAuthn to inspect the evidence payload and authorize the action.
  • Execution Worker (Lambda or Fargate task): Activated strictly by the approval.approved webhook. It reads the approval record, atomically marks it as consumed, and uses backend credentials to perform the final side effect.

When engineering reviews scrutinize your architecture, data residency and hosting parameters will come up. AgentDraft is a proprietary hosted API; it is not open source and is not offered as a self-hosted or on-premise product. The approval tokens and audit chains reside securely within AgentDraft's multi-tenant infrastructure.

The audit question: proving a human approved it

When an autonomous workflow goes wrong in production, internal compliance teams and external auditors ask a specific set of forensic questions. They do not care about raw CloudWatch logs showing that an LLM produced a completion. They demand verifiable answers to four questions:

  1. Which human principal authorized this exact action?
  2. What precise data parameters were presented to the reviewer at the moment of approval?
  3. Did the parameters executed by the downstream tool match the approved evidence payload byte-for-byte?
  4. Was this specific approval consumed more than once?

AgentDraft records state-changing agent actions in an append-only audit trail. This structure guarantees that historical events cannot be modified or truncated retroactively. The audit record stores the complete transaction envelope:

{
  "audit_id": "aud_rec_01J98X4B6R9Z0Q",
  "workspace_id": "ws_live_77102",
  "agent_id": "ag_bedrock_claims_processor",
  "action_class": "billing.refund",
  "action_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
  "decision": "APPROVED",
  "decided_by": {
    "user_id": "usr_9921",
    "auth_method": "webauthn_fido2",
    "ip_address": "198.51.100.42"
  },
  "decision_timestamp": "2026-10-17T09:14:22.104Z",
  "reviewer_note": "Verified customer receipt manually.",
  "consumed_at": "2026-10-17T09:14:23.011Z",
  "consumed_by_execution_id": "exec_lambda_node22_8819b"
}

Compliance and retention policies require careful planning. Audit retention is per-tier and enforced on read as well as on write, so the retention claim holds even though deletion is lazy. The free Developer tier includes 7-day audit retention, which is ideal for integration sandboxes and continuous testing. Production enterprise workflows subject to multi-year recordkeeping requirements require a higher plan tier with extended retention.

Be aware of compliance boundaries during architectural reviews: AgentDraft does not hold formal compliance certifications (SOC 2, HIPAA, ISO 27001, etc.). It does keep an append-only audit trail. Teams operating in regulated spaces must rely on the cryptographic immutability of the audit logs to satisfy their own internal compliance baselines.

Wiring it up: a minimal end-to-end sequence

The sequence below shows how to implement a complete, robust human-in-the-loop approval for Bedrock agents without hitting race conditions or Lambda timeouts.

  1. Intent Generation: The Bedrock Agent processes user input, selects an action group tool, and passes arguments to the backing Lambda.
  2. Deterministic Hashing: The Lambda normalizes the JSON arguments and computes an action_hash.
  3. Local State Committal: The Lambda records a pending task in a local DynamoDB table keyed by idempotency_key to ensure that even if Bedrock crashes, the action request state is tracked.
  4. Open Approval Request: The Lambda calls POST /v1/approvals on AgentDraft with the action hash, one-line summary, and evidence payload using an API key scoped to approvals:request.
  5. Session Release: The Lambda returns an HTTP 202 Accepted payload through Bedrock's schema format, instructing the model to inform the user that their request is pending managerial review. Bedrock gracefully ends the turn.
  6. Human Decision: The workspace owner receives an email notification with a direct link to the AgentDraft dashboard, authenticates via passkey, reviews the raw payload, and approves the action with an optional note.
  7. Webhook Dispatch: AgentDraft emits an approval.approved event carrying the request ID and signature header to your API Gateway endpoint.
  8. Signature and Timestamp Check: Your receiver Lambda validates that the delivery was signed using your workspace secret and verifies that the timestamp skew is under 300 seconds.
  9. Atomic Consumption: The receiver Lambda performs an atomic conditional update to transition the record from approved to consumed. If the update fails because the record is already consumed, execution halts immediately.
  10. Consequential Execution: With the approval safely claimed, the worker issues the final command to the underlying resource (e.g., executing the transaction, or dispatching an email).

When working with calendar coordination tools, watch out for physical execution limits. AgentDraft coordinates holds and commits through a priority-aware conflict engine so multiple agents can act on the same calendar without double-booking. The conflict engine is race-free at the storage layer, not in application code. A booking writes one time-bucket row per 5-minute bucket (so a 30-minute booking writes 6 rows) inside a single DynamoDB TransactWriteItems, and each write carries a ConditionExpression encoding the priority rule — so two agents committing the same slot cannot both win.

As documented in the Amazon DynamoDB Developer Guide on TransactWriteItems, transactional operations have a strict limit of 100 item writes per batch. Because AgentDraft reserves one item slot for coordinating transaction metadata, bookings are capped at 99 five-minute buckets (480 minutes total). If an agent attempts to submit an approval request to commit a continuous 9-hour block (108 buckets), the calendar engine will reject the write with a 422 booking_too_long error. The calling Bedrock agent must be engineered to catch this 422 response and split the requested duration into distinct, sequential reservations.

What this does not solve yet

Building transparent agent architectures requires understanding where current platform boundaries lie. Implementing human-in-the-loop approval for Bedrock agents using this pattern handles durable pause-and-resume workflows, but developers must design around several current platform constraints.

The requesting agent decides for itself when to open an approval request. AgentDraft does not yet provide a policy engine that auto-requires approval by action class, amount threshold, or role, and there are no escalation chains or multi-approver quorums — a single workspace human resolves each request.

This means your action group Lambda must enforce your business rules programmatically. If you want low-value transactions to process automatically while high-value transactions require a human gate, your Lambda must evaluate whether the transaction exceeds your threshold and decide whether to call /v1/approvals or execute immediately.

Second, review latency remains an operational bottleneck. If a human reviewer is offline, asleep, or unavailable, an approval request will sit in the pending state indefinitely. Bedrock cannot keep a user-facing session open waiting for that human. You must implement a sweeping dead-letter task or TTL check on your internal orchestration table that marks expired requests as rejected after a designated timeframe, returning an explicit timeout notification to the end user.

Third, access management relies strictly on native primitives. Enterprise SSO (SAML/SCIM via WorkOS) is on the AgentDraft roadmap and not available today; agents authenticate with bearer API keys and humans with passkeys. Teams requiring Okta-based SCIM provisioning must map user accounts manually during onboarding.

Finally, calendar synchronizations are bounded by current provider support: AgentDraft syncs Google Calendar today; Microsoft 365 / Outlook calendar sync is planned, not yet shipped. If your Bedrock agents manage executive schedules across hybrid enterprise domains, you must limit automated calendar holds to Google Calendar workspaces until the additional connectors leave active development.

Developers who need deeper architectural details on audit boundaries, rate envelopes, and storage mechanisms can review our comprehensive technical specification.

Frequently Asked Questions

How do I add a human approval step in a Bedrock agent flow?

You split the workflow at the action group Lambda boundary. When an agent requests a consequential action, the Lambda computes a deterministic hash of the normalized parameters, writes a pending record to a local database, and submits an approval request carrying the payload to an external gate API. The Lambda immediately returns an HTTP 202 response to Bedrock with a message that the action is pending review. When a human reviews and approves the evidence in the dashboard, a signed webhook notifies your backend worker to resume execution.

Can I implement human-in-the-loop approval for Bedrock agents without a third-party service?

Yes, but you must construct the entire durable state machine yourself. This requires building a secure database to track pending states, writing an authenticated frontend dashboard for human operators, engineering signed webhook or event-polling pipelines to alert your workers, and managing append-only logs for audit retention. Implementing this inside raw Bedrock prompt context is impossible because prompts are advisory, non-deterministic, and cannot survive Lambda session terminations.

How does the agent know an approval was granted after the session ends?

The Bedrock agent session that initiated the request exits as soon as it returns the "pending approval" message. It does not wait. Resumption is driven asynchronously: upon human approval, the external gate fires an approval.approved webhook payload to an API Gateway endpoint. A standalone worker Lambda receives this event, verifies the signature, and directly triggers the downstream execution task. If the conversation with the user must continue, your backend uses Bedrock's sessionId and runtime APIs to inject the completion message into the dialogue thread.

What stops an approved action from running twice if the Lambda retries?

Deduplication requires an atomic state transition to a "consumed" state at the database layer. Before your worker Lambda triggers an approved action, it executes an atomic conditional update (such as a DynamoDB attribute_exists condition check) verifying that the approval status is APPROVED and transitioning it to CONSUMED in the same operation. If a Lambda retries due to a network glitch, the retried execution fails the condition check and exits immediately without executing the downstream side effect.

Where do approvers actually click approve, and can it be done from email?

Approvals must be decided inside an authenticated web dashboard. AgentDraft sends an email notification containing a secure link to the dashboard, but the operator must log in using a passkey (WebAuthn) or recovery magic link to execute the decision. Approving directly via an email link is unsupported by design: automated email security scanners and link pre-fetchers regularly trigger unauthenticated HTTP links, which would lead to unauthorized actions being approved automatically.

To inspect endpoints and start building, create an account on AgentDraft. The free Developer tier requires no card and includes 1 user, 3 agents, and one mailbox per workspace.