# AWS Bedrock Guardrails — Implementing an Allowlist > When building AI agents with AWS Bedrock, guardrails are your first line of defense for keeping conversations on topic. A common requirement is to restrict an agent to a pre-defined strict allowlist, blocking everything… - URL: https://lokahq.github.io/tech-blog/aws-bedrock-guardrails-implementing-an-allowlist/ - Type: Blog article - Authors: Guilherme Ribeiro (Advanced ML Engineer) - Published: 2026-07-01 - Updated: 2026-09-30 - Reading time: 7 min - Tags: Amazon Bedrock Guardrails, AI Agents, Strands Agents, Allowlisting - Originally published on Medium: https://medium.com/loka-engineering/aws-bedrock-guardrails-implementing-an-allowlist-243bc94a7a12 --- 4 Practical Approaches When building AI agents with AWS Bedrock, guardrails are your first line of defense for keeping conversations on topic. A common requirement is to restrict an agent to a pre-defined strict allowlist, blocking everything else. Consider you’re building a financial assistant that should strictly answer questions about investments, markets, or personal finance. Your developer intuition would most likely tell you to strictly define what constitutes a question around investments, markets, etc., and then block any question that does not pass that test. The problem? Bedrock Guardrails only supports denied topics. There is no native allowlist concept. This post walks through three approaches to work around that constraint. ## The Core Problem AWS Bedrock Guardrails lets you define **denied topics**. These are categories of conversation that should be blocked. Each topic is described in natural language, optionally with example phrases, and the service uses that description to classify incoming content. The [API schema](https://docs.aws.amazon.com/bedrock/latest/APIReference/API_GuardrailTopic.html) makes the limitation explicit: the _type_ field only accepts _DENY_. There is no _ALLOW_ value. Consider our previous example of a financial assistant. To express those constraints using Guardrails, your only tool is a denylist. There, you can only define what to block, but not what to allow. That’s the gap we need to work around. ```json { "name”: "Off-topic Requests", "definition": """ Any discussion unrelated to investments, personal finance, banking, credit, or financial markets such as cooking, sports, travel, or general knowledge. """, "type": "DENY" } ``` This looks reasonable at first, but it’s explicitly unsupported. The [AWS Bedrock Guardrails documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-denied-topics.html#guardrails-denied-topics-best-practices) states: > Don’t define negative topics or exceptions. For example, ‘All contents except medical information’ or ‘Contents not containing medical information’ are negative definitions of a topic and must not be used. The definition above, “Any discussion unrelated to investments…”, is exactly this anti-pattern. AWS Bedrock Guardrails require clear, affirmative definitions of what to detect. A negation like “everything that is not finance” falls outside what the classifier is designed to handle, and AWS makes no guarantees about its behavior. Beyond being unsupported, it also doesn’t scale. The allowed set (financial topics) is small and well-defined, while its complement is essentially infinite. AWS Bedrock Guardrails have a hard limit of 30 denied topics per guardrail, so even if you tried to enumerate off-topic subjects you’d exhaust your budget long before getting close to complete coverage. So, if you need a true allowlist, you have to get creative. ## Approach 1 — System Prompt as Allowlist The simplest option: define the allowed topics directly in the system prompt and instruct the agent to respond with a static fallback message for anything outside that scope. ```python from strands import Agent SYSTEM_PROMPT = """ You are a financial assistant. You may ONLY answer questions about: - Investments (stocks, bonds, ETFs, mutual funds) - Personal finance (budgeting, saving, retirement planning) - Banking and credit - Financial markets and economic indicators If the user asks about anything outside the financial domain, respond exactly with: "I'm only able to help with financial topics. Please ask me about investments, banking, or personal finance." Do not deviate from this message for off-topic requests. """ agent = Agent(system_prompt=SYSTEM_PROMPT) ``` **Pros**: Simple to implement, keeps the standard integration with AWS Bedrock Guardrails intact, no extra API calls. **Cons**: Relies on the LLM to enforce the boundary. A sufficiently creative prompt might coax the model into going off-script. It’s a soft boundary, not an infrastructure-level one. ## Approach 2 — Invert the Denied Topics List This is the clever one. Since AWS Bedrock Guardrails can only deny, the trick is to define your allowed topics as denied topics. Then you’d call the _ApplyGuardrail_ API directly, before the agent, using the guardrail’s intervention as a signal that the topic is allowed. This is useful because _ApplyGuardrail_ runs independently from model inference. You can evaluate the user request before retrieval, tool execution, or the agent loop begins, instead of relying on the agent itself to decide whether the request is in scope. The idea is to populate the denied topics list with your allowed topics. In this case, financial subjects like investments, banking, and personal finance. Then call _ApplyGuardrail_ directly before the agent and invert the result: ![Flowchart for an allowlist built by inverting Bedrock Guardrails: matching financial topics are allowed and unmatched topics are blocked.](https://lokahq.github.io/tech-blog/blog/aws-bedrock-guardrails-implementing-an-allowlist/1-DgNxEFbOtUct3KjfaRW2gA.webp) Allowlist decision flow: ApplyGuardrail intervention means the matched financial topic is allowed; no intervention means the input is blocked. 1. Call _ApplyGuardrail_ with the user’s input 2. If the guardrail intervenes → the input matched a financial topic → it’s within the allowed domain → let it through 3. If the guardrail does not intervene → the input didn’t match any financial topic → it’s off-topic → block it ```python import boto3 bedrock = boto3.client("bedrock-runtime", region_name="us-east-1") def is_allowed_topic(user_input: str) -> bool: response = bedrock.apply_guardrail( guardrailIdentifier="your-guardrail-id", guardrailVersion="DRAFT", source="INPUT", content=[{"text": {"text": user_input}}], ) # Guardrail intervenes = matched a financial topic = within allowed domain = allow it # Guardrail silent = no financial topic matched = off-topic = block it return response["action"] == "GUARDRAIL_INTERVENED" ``` **Pros**: Pre-model gating, operating independently of the main agent’s instructions and subsequent behaviour. This offers flexibility in working with the guardrails to suit your needs. You can apply the same guardrail for content safety policies like PII, violence and prompt injection or use two different ones: one for permitted topics and another for content policies. **Cons**: There is a hard limit of 30 denied topics per guardrail, so this only works if your “allowed-list” is enumerable. **Note**: AWS themselves documented this exact pattern in a [Builder Center article](https://builder.aws.com/content/2yhCsrdhxihOIh8gUFuJznUAxth/on-explicit-allows-with-amazon-bedrock-guardrails). ## Approach 3 — Strands Hook + Inverted AWS Bedrock Guardrails This approach leans into the Strands SDK’s [hook system](https://strandsagents.com/docs/user-guide/concepts/agents/hooks/) to intercept the request before the agent invocation begins. Strands provides a _BeforeInvocationEvent_ that fires once per user request, before the agent loop starts. The event exposes both a messages field (the full conversation so far) and a cancel field: set it, and the invocation is aborted with your custom message. **Note**: _BeforeModelCallEvent_ also has a cancel field but fires before every model call, including intermediate tool-use rounds, and does not expose messages. For topic classification, _BeforeInvocationEvent_ is the right hook: it runs once, at the entry point. ```python from strands import Agent from strands.hooks import BeforeInvocationEvent async def topic_guard_hook(event: BeforeInvocationEvent) -> None: user_input = event.messages[-1]["content"] if not await is_allowed_topic(user_input): event.cancel = "I'm only able to help with financial topics. Please ask me about investments, banking, or personal finance." agent = Agent(system_prompt=SYSTEM_PROMPT) agent.add_hook(topic_guard_hook, BeforeInvocationEvent) ``` ## Making It Fast: Async Parallel Checks The real win here is that you can run both the allowlist classification and the content safety guardrail concurrently, so total latency is the max of the two, not their sum. For this, you can use _asyncio.to\_thread_ to make apply\_guardrail awaitable: ```python import asyncio import boto3 from strands import Agent from strands.hooks import BeforeInvocationEvent bedrock = boto3.client("bedrock-runtime", region_name="us-east-1") async def apply_guardrail_async(text: str, guardrail_id: str, version: str, source: str) -> dict: return await asyncio.to_thread( bedrock.apply_guardrail, guardrailIdentifier=guardrail_id, guardrailVersion=version, source=source, content=[{"text": {"text": text}}] ) async def topic_guard_hook(event: BeforeInvocationEvent) -> None: user_input = event.messages[-1]["content"] # Run both checks concurrently allowlist_check, content_safety_check = await asyncio.gather( apply_guardrail_async(user_input, "allowlist-guardrail-id", "DRAFT", "INPUT"), apply_guardrail_async(user_input, "content-safety-guardrail-id", "DRAFT", "INPUT"), ) # Allowlist: guardrail silent = no financial topic matched = off-topic (inverted logic from Approach B) if allowlist_check["action"] == "NONE": event.cancel = "I'm only able to help with financial topics. Please ask me about investments, banking, or personal finance." return # Content safety: standard logic — intervenes = blocked content if content_safety_check["action"] == "GUARDRAIL_INTERVENED": event.cancel = "I'm unable to respond to that request." agent = Agent(system_prompt=SYSTEM_PROMPT) agent.add_hook(topic_guard_hook, BeforeInvocationEvent) ``` ## Approach 4 — Strands Hook + Lightweight LLM This one is a variant of the Hooks approach. It still uses the _BeforeInvocationEvent_ to filter the content, however, instead of inverting the Guardrails denied list as your allowed list, you can use a fast model like [Claude Haiku 4.5](https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-anthropic-claude-haiku-4-5.html) as your topic classifier. This gives you more flexibility in defining the allowed domain without being constrained to 30 topics, and the natural-language definition can be more nuanced. ```python import asyncio import json import boto3 bedrock = boto3.client("bedrock-runtime", region_name="us-east-1") CLASSIFIER_PROMPT = """ You are a topic classifier for a financial assistant. Determine if the user's question is within the financial domain. The financial domain includes: investments (stocks, bonds, ETFs), personal finance, banking, credit, financial markets, and economic indicators. Respond with JSON only: {"allowed": true} or {"allowed": false} """ async def classify_topic_with_llm(user_input: str) -> bool: response = await asyncio.to_thread( bedrock.converse, modelId="us.anthropic.claude-haiku-4-5-20251001-v1:0", system=[{"text": CLASSIFIER_PROMPT}], messages=[{"role": "user", "content": [{"text": user_input}]}], inferenceConfig={"maxTokens": 50, "temperature": 0}, ) result = json.loads(response["output"]["message"]["content"][0]["text"]) return result.get("allowed", False) async def topic_guard_hook(event: BeforeInvocationEvent) -> None: user_input = event.messages[-1]["content"] # Run allowlist check (LLM) and content safety check (Guardrails) concurrently is_allowed, safety_check = await asyncio.gather( classify_topic_with_llm(user_input), apply_guardrail_async(user_input, "content-safety-guardrail-id", "DRAFT", "INPUT"), ) if not is_allowed: event.cancel = "I'm only able to help with financial topics. Please ask me about investments, banking, or personal finance." return if safety_check["action"] == "GUARDRAIL_INTERVENED": event.cancel = "I'm unable to respond to that request." ``` **Pros**: Pre-model enforcement (no LLM tokens wasted on blocked requests), concurrent execution keeps latency low, clean separation of allowlist logic from content safety, flexible classifier definition. **Cons**: Adds a classifier LLM call to every request (cost and latency of Haiku is low but non-zero). The Guardrails inverted-logic variant might be cheaper but inherits the 30-topic limit. ## Comparing the Four Approaches | Approach | Best for | Enforcement point | Topic scope | Extra latency | Main trade-off | | -------------------------------------- | ----------------------------------------- | ---------------------- | ---------------------- | ---------------------------------------------------------- | ----------------------------------------------------------------- | | System prompt | Quick prototypes | Inside the main LLM | Flexible | None | Easy, but relies on the model following instructions | | Inverted Guardrails | Small, well-defined domains | Before the agent/model | Up to 30 denied topics | +1 ApplyGuardrail call | Model-independent from the main LLM, but still classifier-based | | App/Strands hook + Inverted Guardrails | Production agent flows with small domains | Before the agent loop | Up to 30 denied topics | +1 ApplyGuardrail call; can run alongside safety checks | Clean lifecycle integration, but inherits Guardrails topic limits | | App/Strands hook + LLM classifier | Broader or nuanced domains | Before the agent loop | Flexible | +1 lightweight model call; can run alongside safety checks | More flexible, but adds cost and another classifier to evaluate | **Note**: Strands hooks are not required for the inverted Guardrails pattern. They are an integration point for Strands-based agents, letting you run the check before the agent loop starts and cancel the invocation when needed. ## Closing Thoughts Bedrock Guardrails does not currently provide a native “allow only these topics” control. But by combining ApplyGuardrail, inverted denied-topic logic, and Strands hooks, you can enforce topic boundaries before the agent loop starts. The key design choice is whether your allowed domain is small enough to enumerate. If it is, inverted Guardrails are simple and model-independent. If it is not, a lightweight classifier gives you more flexibility, at the cost of an extra model call. In either case, treat the allowlist check and safety guardrails as separate layers. Topic scoping decides whether the request belongs to your product domain. Safety guardrails decide whether the request is acceptable to process at all. If you’ve hit this constraint before or found another workaround that was not covered here, we’d love to hear about it. ## References - [Amazon Bedrock Guardrails — Denied Topics](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-denied-topics.html) - [GuardrailTopic API schema](https://docs.aws.amazon.com/bedrock/latest/APIReference/API_GuardrailTopic.html) - [ApplyGuardrail API Reference](https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ApplyGuardrail.html) - [Use the ApplyGuardrail API independently](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-use-independent-api.html) - [On Explicit Allows with Amazon Bedrock Guardrails — AWS Builder Center](https://builder.aws.com/content/2yhCsrdhxihOIh8gUFuJznUAxth/on-explicit-allows-with-amazon-bedrock-guardrails) - [Strands Agents — Hooks documentation](https://strandsagents.com/docs/user-guide/concepts/agents/hooks/) - [Strands Agents — Hook events API reference](https://strandsagents.com/docs/api/python/strands.hooks.events/) - [Claude Haiku 4.5 Model Card — AWS Bedrock](https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-anthropic-claude-haiku-4-5.html)