Bedrock — Guardrails, Knowledge Bases, Agents & Batch¶
The managed features around the model call. Behaviour (what a guardrail blocks, what a knowledge base retrieves) follows the AWS documentation as of 2026-10-21.
Guardrails¶
A guardrail is a named, versioned set of safety policies applied to model input and output. Create it once on the control plane (bedrock), version it, then reference id + version at inference time.
| Policy | What it does | Key settings |
|---|---|---|
| Content filters | Detect harmful text or images in six categories: SEXUAL, VIOLENCE, HATE, INSULTS, MISCONDUCT, PROMPT_ATTACK |
Strength NONE/LOW/MEDIUM/HIGH separately for input and output; action BLOCK or NONE (detect only); modalities TEXT, IMAGE |
| Denied topics | Block topics you define in words | name, definition, examples; type DENY. Definitions are limited to 200 characters (Classic) or 1,000 (Standard) |
| Word filters | Block exact words and phrases (case-insensitive, whole word) and a managed PROFANITY list |
wordsConfig, managedWordListsConfig |
| Sensitive information | Detect or mask PII and secrets, plus custom regexes | 30+ entity types (EMAIL, NAME, US_SOCIAL_SECURITY_NUMBER, CREDIT_DEBIT_CARD_NUMBER, AWS_ACCESS_KEY, ...); action BLOCK, ANONYMIZE (replaced with {NAME}-style tags), NONE |
| Contextual grounding | Flag answers not supported by a source or irrelevant to the query (RAG hallucination check) | Types GROUNDING, RELEVANCE; a threshold between 0 and 1 |
| Automated Reasoning | Check answers against formal logic rules you supply (automatedReasoningPolicyConfig) |
Not covered here |
Tiers: CLASSIC supports English, French and Spanish. STANDARD has broader language and code support, prompt-leakage detection and is "more robust"; it uses cross-Region inference (the boto3 request has crossRegionConfig.guardrailProfileIdentifier). Standard tier for contextual grounding exists only in a subset of Regions; elsewhere the request fails with a validation error.
Create and version¶
import boto3
bedrock = boto3.client("bedrock", region_name="us-east-1") # control plane
guardrail = bedrock.create_guardrail(
name="support-bot-guardrail",
blockedInputMessaging="Sorry, I can't help with that request.",
blockedOutputsMessaging="Sorry, I can't share that response.",
contentPolicyConfig={"filtersConfig": [
{"type": "PROMPT_ATTACK", "inputStrength": "HIGH", "outputStrength": "NONE"},
{"type": "HATE", "inputStrength": "MEDIUM", "outputStrength": "MEDIUM"},
]},
topicPolicyConfig={"topicsConfig": [{
"name": "Investment advice", "type": "DENY",
"definition": "Recommendations about buying or selling financial products.",
"examples": ["Which stock should I buy?"],
}]},
sensitiveInformationPolicyConfig={"piiEntitiesConfig": [
{"type": "EMAIL", "action": "ANONYMIZE"},
{"type": "US_SOCIAL_SECURITY_NUMBER", "action": "BLOCK"},
]},
)
gid = guardrail["guardrailId"] # working copy is version "DRAFT"
# creation is asynchronous: poll bedrock.get_guardrail(guardrailIdentifier=gid)["status"] until "READY"
version = bedrock.create_guardrail_version(guardrailIdentifier=gid, description="initial")["version"] # "1"
The status values in the service model are CREATING, UPDATING, VERSIONING, READY, FAILED, DELETING. Use DRAFT for experiments and a numbered version in production so a console edit cannot change behaviour under you. Manage guardrails as code (Terraform, CDK, a script) and test them (below).
Attach to Converse¶
resp = client.converse(
modelId=MODEL_ID,
messages=[{"role": "user", "content": [
{"text": "Answer politely."}, # not evaluated once guardContent is present
{"guardContent": {"text": {"text": user_input}}}, # evaluated
]}],
guardrailConfig={"guardrailIdentifier": gid, "guardrailVersion": version, "trace": "enabled"},
)
if resp["stopReason"] == "guardrail_intervened":
... # blocked OR masked: see below
| Behaviour (AWS docs) | Detail |
|---|---|
No guardContent |
The guardrail evaluates the whole prompt (system prompts excluded unless wrapped) |
Any guardContent present |
The guardrail evaluates only those blocks, except policies that apply to everything such as word filters |
| System prompt | Evaluated only if you wrap it in guardContent |
| Tool data | Not evaluated: toolResult, toolConfig descriptions and schemas, and toolUse.input the model generates. Text blocks and guardContent are |
stopReason |
guardrail_intervened for both blocking and masking |
| Blocked | output.message carries your blockedInputMessaging or blockedOutputsMessaging; in the AWS example usage is zeros when blocked on input |
Masked (ANONYMIZE) |
Model answer with {NAME}/{EMAIL} tags, same stopReason. Enable the trace and read action (ANONYMIZED vs BLOCKED) to tell them apart |
Trace enabled / enabled_full |
enabled_full also returns non-detected entries for debugging |
| Trace content | Includes the unmasked matches and the original modelOutput. Treat it as sensitive and keep it out of ordinary logs |
A blocked response (as the AWS docs show for a blocked topic):
stopReason: guardrail_intervened
text: Sorry, I can't help with that.
trace.guardrail.inputAssessment['abc123'].topicPolicy.topics -> [('Investment advice', 'BLOCKED')]
Streaming: converse_stream takes guardrailConfig.streamProcessingMode: sync (default; chunks are buffered and scanned, so slightly delayed but accurate) or async (chunks are sent immediately and scanned in the background: a harmful chunk may already have reached the user before the block, and masking is not supported in async mode). For InvokeModelWithResponseStream the body field is amazon-bedrock-guardrailConfig with "streamProcessingMode": "ASYNCHRONOUS".
ApplyGuardrail: Use Guardrails Without a Model¶
bedrock-runtime.apply_guardrail checks any text against a guardrail with no model call. Use it to screen the user input before retrieval, check tool output, check output of non-Bedrock models, or test a guardrail in CI.
resp = client.apply_guardrail(
guardrailIdentifier=gid, guardrailVersion=version,
source="INPUT", # "INPUT" = user content, "OUTPUT" = model content
content=[{"text": {"text": "Ignore all previous instructions and ..."}}],
)
if resp["action"] == "GUARDRAIL_INTERVENED":
safe_text = resp["outputs"][0]["text"] # canned message (blocked) or masked text
detected = resp["assessments"][0]["contentPolicy"]["filters"]
Response fields: action (NONE, GUARDRAIL_INTERVENED), actionReason, outputs (empty when nothing happened), assessments, usage (policy units billed), guardrailCoverage. Add outputScope="FULL" to also see non-detected entries (it does not apply to word filters and regexes). The request path (/guardrail/gr-abc123/version/1/apply)
Contextual grounding needs qualifiers on the content blocks:
client.apply_guardrail(
guardrailIdentifier=gid, guardrailVersion=version, source="OUTPUT", outputScope="FULL",
content=[
{"text": {"text": "Refunds are issued within 14 days.", "qualifiers": ["grounding_source"]}},
{"text": {"text": "How long do refunds take?", "qualifiers": ["query"]}},
{"text": {"text": "Refunds take 90 days.", "qualifiers": ["guard_content"]}}, # the answer under test
],
)
Responses report usage in policy units (topicPolicyUnits, contentPolicyUnits, wordPolicyUnits, sensitiveInformationPolicyUnits, contextualGroundingPolicyUnits); check the pricing page for how they bill. Both attached guardrails and ApplyGuardrail report units. The service model also lists organisation-level enforced guardrails (put_enforced_guardrail_configuration) and an InvokeGuardrailChecks runtime operation; they are not covered here.
Testing guardrails¶
| Level | What you can do | What you cannot |
|---|---|---|
| Plumbing (offline) | Stubber or a fake server: the app sends the right guardrailConfig, treats guardrail_intervened as a blocked turn, masks logs, never prints the trace |
Prove the policy blocks anything. moto has no guardrails (create_guardrail raises NotImplementedError) |
| Policy (live, in a test account) | A dataset of attack, benign and PII prompts run through apply_guardrail (no model cost): assert action per case, track false-positive and false-negative rates |
Cover every phrasing; classifiers are probabilistic: use thresholds over a dataset, not one example |
| Red teaming | Generate attacks with an eval tool; see Red Teaming and the OWASP LLM guide | Replace layered defences: a guardrail is one layer, not an authorisation system |
Knowledge Bases (RAG)¶
A knowledge base ingests your documents, embeds them into a vector store and retrieves chunks at query time. AWS now has two kinds: Managed (Bedrock manages ingestion, indexing, storage, reranking; connectors for S3, SharePoint, Confluence, Google Drive, OneDrive, web crawler; document-level ACL filtering; agentic multi-hop retrieval) and Customer-managed (your vector store such as OpenSearch Serverless, Aurora or Neptune Analytics, and full control of parsing). AWS recommends Managed for new work. Third-party connectors, ACLs and native AgentCore Gateway integration are Managed-only.
| Client | Operations |
|---|---|
bedrock-agent |
create_knowledge_base, create_data_source, start_ingestion_job, get_ingestion_job, ingest_knowledge_base_documents |
bedrock-agent-runtime |
retrieve, retrieve_and_generate (+ …_stream), rerank, generate_query, agentic_retrieve_stream |
runtime = boto3.client("bedrock-agent-runtime", region_name="us-east-1")
# Retrieve: you get ranked chunks and build the prompt yourself (best for control and for testing retrieval alone)
out = runtime.retrieve(
knowledgeBaseId="KB12345678",
retrievalQuery={"text": "refund window"},
retrievalConfiguration={"vectorSearchConfiguration": {"numberOfResults": 5}}, # overrideSearchType HYBRID or SEMANTIC, filter, reranking
)
for r in out["retrievalResults"]:
print(round(r["score"], 2), r["location"]["s3Location"]["uri"], r["content"]["text"][:80])
# RetrieveAndGenerate: retrieval + a model call + citations in one request
cfg = {"type": "KNOWLEDGE_BASE", "knowledgeBaseConfiguration": {
"knowledgeBaseId": "KB12345678",
"modelArn": "arn:aws:bedrock:us-east-1:123456789012:inference-profile/global.anthropic.claude-haiku-5-5"}}
ans = runtime.retrieve_and_generate(input={"text": "How long do refunds take?"}, retrieveAndGenerateConfiguration=cfg)
print(ans["output"]["text"], ans["sessionId"]) # pass sessionId back to continue the conversation
refs = [ref for c in ans["citations"] for ref in c["retrievedReferences"]]
retrieve_and_generate takes a model ARN (a foundation model or an inference profile ARN), not a bare ID. The model must support Knowledge Bases (Claude Sonnet 5.5 and Haiku 5.5 list it; Nova 2 Lite does not). Retrieve also accepts a guardrailConfiguration (its response carries guardrailAction) and, for permission-filtered Managed bases, a userContext.userId.
Test it in layers:
| Concern | Test |
|---|---|
Request building (filters, numberOfResults, model ARN) |
Stubber with expected_params |
| Prompt assembly from chunks | Pure function: pass canned retrievalResults |
| Retrieval quality | A small labelled set of questions with the expected source document; assert the expected s3Location.uri is in the top k. Needs a live knowledge base in a test account |
| Grounded answers | ans["citations"] must be non-empty and the cited text must support the claim; score faithfulness and context precision/recall with the DeepEval RAG metrics; see RAG testing |
| Ingestion | After start_ingestion_job, poll get_ingestion_job until complete; assert failed-document counts in test; ingestion takes time, so keep it out of unit tests |
| Access control | Different userContext.userId values return different documents (Managed bases with ACLs) |
moto implements only control-plane CRUD for bedrock-agent agents and knowledge bases, not retrieve or retrieve_and_generate. Wrap the runtime client in a small Retriever class and fake that.
Agents: Classic and AgentCore¶
Agents Classic (bedrock-agent, bedrock-agent-runtime) |
AgentCore (bedrock-agentcore, bedrock-agentcore-control) |
|
|---|---|---|
| Status | "No longer open to new customers"; existing customers continue (AWS notice: "maintenance mode") | The recommended path; AWS says to "explore Amazon Bedrock AgentCore" for similar capabilities |
| Model | Configured agent: model + instructions + action groups (Lambda or return-of-control) + knowledge bases; versions and aliases | You host your own agent code (any framework) in a managed runtime with session isolation; plus memory, gateway, browser, code interpreter, policy and evaluation services |
| Call | invoke_agent(agentId, agentAliasId, sessionId, inputText, enableTrace=...); the answer arrives as an event stream completion of chunk (bytes) events; test alias TSTALIASID |
invoke_agent_runtime(agentRuntimeArn=..., runtimeSessionId=..., payload=bytes) returns a streaming response |
# AgentCore: invoke a deployed agent runtime (shape from the AWS developer guide)
import json
import boto3
agentcore = boto3.client("bedrock-agentcore", region_name="us-east-1")
resp = agentcore.invoke_agent_runtime(
agentRuntimeArn="arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/support_agent-AbC123", # placeholder
runtimeSessionId=session_id, # same id = same conversation; use a UUID per conversation
payload=json.dumps({"prompt": "Where is order ord-42?"}).encode(),
)
if "text/event-stream" in resp.get("contentType", ""):
for line in resp["response"].iter_lines(chunk_size=10):
if line.startswith(b"data: "):
print(line[6:].decode())
else:
print(json.loads(b"".join(resp["response"])))
# Agents Classic
runtime = boto3.client("bedrock-agent-runtime", region_name="us-east-1")
resp = runtime.invoke_agent(agentId="AGENT12345", agentAliasId="TSTALIASID", sessionId="s-1",
inputText="Where is order ord-42?", enableTrace=True)
answer = "".join(e["chunk"]["bytes"].decode() for e in resp["completion"] if "chunk" in e)
Facts and cautions from the AgentCore developer guide:
- The payload is binary, up to 100 MB; responses stream.
ValidationException,ResourceNotFoundException,AccessDeniedException,ThrottlingExceptionand a retryableRetryableConflictException(409, "Session operation in progress") can occur. - If the agent uses OAuth inbound auth, the AWS SDK cannot be used: make an HTTPS request instead.
- Validate that
promptis a string in the agent entrypoint. The payload arrives as parsed JSON; a non-string value containing atoolUseblock could make some frameworks run the named tool directly, bypassing model reasoning and guardrails. This is a test case worth writing (see the OWASP LLM guide). - IAM:
bedrock-agentcore:InvokeAgentRuntimefor callers. InvokeAgentRuntimeCommandruns deterministic shell commands (tests, builds) in a session without going through the LLM.
Testing agents is mostly testing your adapter: keep one class (AgentClient.ask(session_id, text) -> AgentAnswer), unit-test your parsing of the event stream with hand-built chunks, test your own agent code locally as ordinary Python before deploying, and run a handful of end-to-end conversations against a deployed test alias or runtime. Judge the behaviour with agent metrics (task completion, tool correctness) from the DeepEval guide and design guidance from Agentic AI Architecture.
Batch Inference¶
For large offline jobs: put JSONL in S3, create a job, collect JSONL from S3 later. Priced separately (see the AWS pricing page), separate quotas, no latency guarantee.
flowchart LR
A["input .jsonl<br/>recordId + modelInput"] -->|"upload"| S3I["s3://bucket/in/"]
S3I --> J["create_model_invocation_job<br/>(bedrock control plane)"]
J -->|"Submitted → Validating → InProgress"| R["Completed / PartiallyCompleted / Failed / Expired"]
R --> S3O["s3://bucket/out/<br/>*.jsonl.out + manifest.json.out"]
import json
def build_batch_jsonl(prompts: dict[str, str], max_tokens=512, system=None) -> str:
lines = []
for record_id, prompt in prompts.items():
model_input = {"messages": [{"role": "user", "content": [{"text": prompt}]}],
"inferenceConfig": {"maxTokens": max_tokens}} # Converse request body, no toolConfig
if system:
model_input["system"] = [{"text": system}]
lines.append(json.dumps({"recordId": record_id, "modelInput": model_input}))
return "\n".join(lines) + "\n"
job = bedrock.create_model_invocation_job(
jobName="nightly-summaries-2026-10-21",
modelId="anthropic.claude-haiku-5-5", # check the model card: batch support is per model
roleArn="arn:aws:iam::123456789012:role/bedrock-batch",
modelInvocationType="Converse", # or "InvokeModel" (native bodies)
inputDataConfig={"s3InputDataConfig": {"s3Uri": "s3://my-batch-bucket/in/"}},
outputDataConfig={"s3OutputDataConfig": {"s3Uri": "s3://my-batch-bucket/out/"}},
)
status = bedrock.get_model_invocation_job(jobIdentifier=job["jobArn"])["status"]
| Fact | Detail |
|---|---|
| Status values | Submitted, Validating, Scheduled, InProgress, Completed, PartiallyCompleted, Failed, Stopping, Stopped, Expired |
Counters on get_model_invocation_job |
totalRecordCount, processedRecordCount, successRecordCount, errorRecordCount; timeoutDurationInHours caps the run |
| Input | JSONL, each line {"recordId": ..., "modelInput": ...}; modelInput is the model's native body (InvokeModel type) or a Converse request body (Converse type). A missing recordId is generated in the output |
| Input limits | Minimum records per job, records per file, file size and job size are quotas: check Service Quotas |
| Output | One output .jsonl per input file: {"recordId", "modelInput", "modelOutput"}; a failed line has an error object (errorCode, errorMessage) instead of modelOutput. A manifest.json.out has record and token totals. Order is not preserved: join on recordId |
| Converse type | toolConfig is rejected (400). Structured output works in both types. For tool calls use the InvokeModel type |
| Not supported | Provisioned models; prompt caching |
| Model support | Per model. The batch table lists Nova 2 Lite, Lite, Micro, Pro, Claude Opus 5 and several 4.x Claude models; the Claude Sonnet 5.5 and Haiku 5.5 cards show batch not supported |
| Completion events | Amazon EventBridge job state-change events replace polling |
Parse results defensively and test the parser with a canned file:
def parse_batch_output(text: str):
ok, failed = {}, {}
for line in text.splitlines():
rec = json.loads(line)
if "modelOutput" in rec and "error" not in rec:
ok[rec["recordId"]] = "".join(b.get("text", "") for b in rec["modelOutput"]["output"]["message"]["content"])
else:
failed[rec["recordId"]] = rec.get("error") or rec
return ok, failed
A job that ends PartiallyCompleted is a normal outcome to handle: re-submit only the failed record ids.
Model Evaluation and Other Features¶
| Feature | What it is | Note |
|---|---|---|
| Evaluation jobs | bedrock.create_evaluation_job(...) with applicationType ModelEvaluation or RagEvaluation, evaluationConfig of kind automated or human, inferenceConfig (models or RAG configs) and an S3 output location |
Metrics and limits are not covered here. For repeatable in-repo evaluation use DeepEval |
| Prompt management and flows | Versioned prompts (promptVariables in Converse) and visual flows |
Not covered |
| Custom and imported models, distillation | bedrock control plane (create_model_customization_job, create_custom_model, create_model_import_job) |
Not covered |
| Intelligent prompt routing | Router profiles (create_prompt_router) |
Listed as unsupported on the 5.5 Claude cards |
Quick Rules¶
- Version guardrails and reference numbered versions in production; manage them as code.
guardrail_intervenedis not always a block. Enable the trace and checkactionbefore deciding to discard an answer, and never log the trace as plain text.- Wrap system prompts and the user turn in
guardContentwhen you want them evaluated, and validate tool arguments and results yourself: guardrails skip them. - Use
ApplyGuardrailto test policies and to screen input before retrieval; keep the dataset of attack and benign prompts in the repo. - Use
Retrievewhen you need control and testability; useRetrieveAndGeneratefor speed. Fail a test when an answer has no citations. - Do not start new projects on Agents Classic. Use AgentCore or a framework, and validate that
promptis a string at the entrypoint. - Batch jobs are keyed by
recordId, not by order; handlePartiallyCompleted; check that the model supports batch. - Keep an adapter class around every agent and knowledge-base call so tests can fake it.
See also¶
- Amazon Bedrock
- 01 Models, Access, Regions & the Converse API
- 02 Streaming, Tools, Structured Output, Multimodal & Caching
- 04 Testing & Production
- OWASP Top 10 for LLM Applications
- DeepEval: RAG Metrics
- DeepEval: Red Teaming
- Agentic AI Architecture
- Guardrails AI (open-source validators)
- Amazon SQS