Building a Denied-Topics Policy With Bedrock Guardrails
10 min read · updated August 11, 2026
A guardrail is a separate Bedrock resource with its own versions, which means you can develop and test one without touching your application at all. That is the part worth learning first, because the alternative — editing a policy and then sending prompts through a real model to see what happens — is slow, expensive, and gives you a blocked message instead of a reason.
Five policies, one resource
CreateGuardrail accepts several independent policy blocks, and a guardrail can carry any combination:
- Content filters (
contentPolicyConfig) — AWS’s predefined categories: Hate, Insults, Sexual, Violence, Misconduct and Prompt Attack, each with an adjustable strength and separate input and output settings. - Denied topics (
topicPolicyConfig) — topics you define, which is the subject of this page. - Word filters (
wordPolicyConfig) — exact matches, plus AWS-managed lists. Competitor names and profanity live here. - Sensitive information (
sensitiveInformationPolicyConfig) — PII entity types and your ownregexesConfigpatterns, each set to block or mask. - Contextual grounding (
contextualGroundingPolicyConfig) — a numericthresholdfor how well a response must be grounded in a source and relevant to the query. This is the one that pairs with a knowledge base.
Two fields are required on every guardrail regardless: blockedInputMessaging and blockedOutputsMessaging, each 1 to 500 characters. These are what your user sees, so write them as product copy rather than as an error. The guardrail name is limited to 50 characters and matches [0-9a-zA-Z-_]+.
Two configuration fields sit outside the policies and are easy to miss because nothing forces you to set them. tierConfig.tierName appears on both the content policy and the topic policy and selects which detection tier evaluates them; the tiers differ in the languages and the breadth of content they cover, so a policy that behaves exactly as intended on English test prompts can behave differently under the tier you did not choose. And crossRegionConfig.guardrailProfileIdentifier lets guardrail evaluation itself route across Regions, in the same spirit as model inference profiles — worth setting if guardrail throughput becomes the constraint rather than the model.
The sensitive-information policy deserves a note of its own, because it is the only one with a non-binary outcome. Each entry in piiEntitiesConfig and regexesConfig carries an action, and masking sits alongside blocking: a masked response comes back with the detected span replaced rather than the whole turn refused. For a support assistant that is usually the better behaviour — an answer with a card number redacted is still an answer, whereas a block is a dead end for the user and a ticket for you. Regex entries additionally take a name and a description, and the name is what appears in the assessment, so name them after what they catch rather than after the pattern they use.
Writing a denied topic
A topic has a name, a definition, optional examples, and a type of DENY. The definition is the part that does the work — it is prose the service uses to decide whether a message is about the topic, so vagueness here is the difference between a policy that blocks what you meant and one that blocks half your traffic.
aws bedrock create-guardrail \
--name support-assistant-topics \
--blocked-input-messaging "I can only help with questions about your account and our products." \
--blocked-outputs-messaging "I can't help with that here. A human agent can pick this up." \
--topic-policy-config '{
"topicsConfig": [
{
"name": "InvestmentAdvice",
"type": "DENY",
"definition": "Recommendations to buy, sell or hold a specific financial instrument, or statements about whether a particular investment is suitable for the user.",
"examples": [
"Should I put my savings into your stock?",
"Is now a good time to buy shares in the company?",
"What should I invest my refund in?"
],
"inputAction": "BLOCK",
"outputAction": "BLOCK"
}
]
}'Write the definition to describe the act, not the subject matter. “Investments” as a definition blocks a user asking why an investment product appears on their statement. The definition above blocks advice and leaves factual questions alone, which is almost always what a legal review actually asked for.
inputAction and outputAction are set per topic, and the asymmetry is useful: you can block a topic on the way out while letting the input through, so the model is allowed to see the question and produce a redirect. Setting both to BLOCK means the user gets your blockedInputMessaging and the model is never called — cheaper, and blunter.
DRAFT is not a version
CreateGuardrail returns HTTP 202 with a guardrailId, a guardrailArn, and a version that AWS documents as always being DRAFT. Editing the guardrail changes DRAFT in place. You create a numbered version with CreateGuardrailVersion, and that numbered version is immutable.
The rule that follows is simple and routinely broken: point production at a number, never at DRAFT. A guardrail referenced as DRAFT means anyone editing the policy in the console has changed production behaviour with no deployment and no review. Use DRAFT while you iterate, cut a version, and promote by changing the version string your application passes.
Testing with ApplyGuardrail
This is the operation that makes guardrails developable. ApplyGuardrail runs the policy against text without invoking any model, so you get the full assessment for a prompt at guardrail cost rather than guessing from a refusal.
aws bedrock-runtime apply-guardrail \
--guardrail-identifier abcd1234efgh \
--guardrail-version DRAFT \
--source INPUT \
--output-scope FULL \
--content '[{"text":{"text":"Should I put my redundancy payout into your stock?"}}]'Three fields in the response carry the answer. action is NONE or GUARDRAIL_INTERVENED. actionReason is the human-readable why. assessments contains a per-policy breakdown, so assessments[0].topicPolicy.topics tells you which named topic matched and with what action.
Use --output-scope FULL while developing. AWS documents that it returns both detected and non-detected entries for enhanced debugging, which is how you see that a topic nearly matched — the default INTERVENTIONS scope only shows you what fired. AWS also notes the full scope does not apply to word filters or to regexes in the sensitive-information policy.
- Collect twenty real prompts: ten you want blocked, ten adjacent ones you want allowed. The second set is the important one.
- Run each through
ApplyGuardrailwith--source INPUTand--output-scope FULL. Recordactionper prompt. - Adjust the topic
definitionandexamplesuntil the two sets separate cleanly. Examples move the boundary more than rewording usually does. - Repeat with
--source OUTPUTagainst representative model responses. Input and output are assessed separately and a policy tuned on one is not tuned on the other. - Cut a numbered version with
CreateGuardrailVersionand keep the twenty prompts as a regression test to re-run before every future version.
Attaching it to a model call
On Converse, add guardrailConfig with a guardrailIdentifier, a guardrailVersion and optionally trace set to enabled.
response = client.converse(
modelId="us.anthropic.claude-sonnet-4-5-20250929-v1:0",
messages=messages,
guardrailConfig={
"guardrailIdentifier": "abcd1234efgh",
"guardrailVersion": "3",
"trace": "enabled",
},
)
if response["stopReason"] == "guardrail_intervened":
assessment = response["trace"]["guardrail"]["inputAssessment"]When a guardrail acts, stopReason comes back as guardrail_intervened — distinct from content_filtered, which is the provider’s own filtering and which your policy cannot influence. With trace enabled you also get trace.guardrail.inputAssessment and outputAssessments, the same structures ApplyGuardrail returns. Log the assessment, not just the fact of the block: “blocked” in a support ticket is unactionable, and “topic InvestmentAdvice matched on input” takes a minute to resolve.
One scoping detail. AWS documents that if you include guardContent blocks in your message content, the guardrail operates only on those blocks; with none, it operates on the whole request. That is how you exempt a large retrieved document from assessment while still guarding the user’s question — worth knowing, because guardrail usage is metered by text units and assessing an entire retrieved corpus on every turn is a real line on the bill.
Using one outside a Bedrock call
ApplyGuardrail is not a testing convenience that happens to exist. It is a standalone content-assessment endpoint that takes text and returns a verdict, with no model involved and no requirement that the text came from Bedrock at all. That makes it usable as a policy layer over a model you host yourself, over a different provider, or over user-generated content that never reaches a model.
The pattern is: call ApplyGuardrail with source: INPUT on the user’s message, run your own inference wherever it lives, then call it again with source: OUTPUT on the generated text. Two round trips instead of the single call guardrailConfig gives you, and in exchange the policy is no longer coupled to where the model runs. If you are consolidating several teams onto one content policy while they use different models, this is how that is done — one guardrail resource, one version number, called explicitly.
Two response fields matter more in this mode than in the attached one. outputs carries the text after any masking has been applied, so for a masking policy this is the value you forward rather than the original — forwarding the input you sent silently discards the redaction you just paid for. And guardrailCoverage reports textCharacters.guarded against textCharacters.total, which is how you verify that the whole message was assessed rather than part of it. A coverage ratio below 1 on a long document is a quiet failure: the guardrail returned NONE because it never looked at the part that mattered.
Streaming is the case that most often forces this shape. With ConverseStream, output assessment cannot happen until enough text exists to assess, so a guardrail on a streamed response either buffers — which removes the reason you were streaming — or evaluates in chunks, which means a violation can be detected after tokens have already reached the user’s screen. There is no configuration that removes that trade-off, because it is a property of streaming rather than of guardrails. If the requirement is that nothing unreviewed is ever displayed, the honest answer is to stop streaming on the paths where it applies, not to reach for a stricter policy.
Finally, treat a guardrail as a control you can evidence, not a guarantee you can promise. Every policy here is probabilistic detection over text, and the reason the twenty-prompt regression set in the previous section is worth keeping is that it is the only artefact that shows what the policy actually does. Attaching the guardrail is ten minutes; the tests are what let you change it a year from now without finding out in production which of the allowed prompts you broke.