Dev.to AI πŸ€– Ai πŸ‘ 0 πŸ“– 20 min read

AgentCore Web Answer: cited web search for AI agents

AgentCore Web Answer has been generally available since November 5, 2026, and I read the announcement twice. Here is why: for every agent with web access, I have built the same chain so far. Call a search, filter the hit

AgentCore Web Answer: cited web search for AI agents

AgentCore Web Answer has been generally available since November 5, 2026, and I read the announcement twice. Here is why: for every agent with web access, I have built the same chain so far. Call a search, filter the hits, read the pages, let the model summarize, somehow carry the sources along. Web Answer takes that whole chain off your plate and returns a finished answer with numbered sources in a single tool call.

Sounds convenient. Still, there are a few catches that the announcement mentions rather quietly: a single Region, English as the launch language and clear rules about what you may do with the results. In this article, I build a small research service for a fictional city library with you, step by step, using the Serverless Framework, TypeScript and properly prepared sources.

In short

Web Answer is a managed tool behind the AgentCore Gateway that answers a question via web search and backs every statement with a source. It saves you from building your own search and summarization pipeline and costs one cent per call. The biggest hurdle is not the code but the frame around it: us-east-1 only, English only at launch and the obligation to always show the sources.

What you will learn in this article

  • When Web Answer fits better than the Web Search tool, and when it does not
  • Gateway, IAM role and Web Answer target in a single serverless.yml
  • A SigV4-signed MCP call from Lambda in TypeScript
  • How to renumber the sources of several answer blocks correctly
  • Which cost, privacy and licensing questions to settle first

What is AgentCore Web Answer?

Web Answer is a so-called connector for Amazon Bedrock AgentCore. You attach it as a target to an AgentCore Gateway. The Gateway is the managed MCP server from AWS: it collects tools behind one URL and speaks the Model Context Protocol (MCP) to the outside world, the standard protocol AI agents use to call tools.

Your agent sends a question in natural language. Web Answer then plans several searches on its own, runs them in parallel, reads the relevant pages and decides when it has enough evidence. What comes back is Markdown text in which statements carry markers such as [[1](https://…)], plus a structured list of sources. AWS runs the model that writes the answer. You do not pick it, and you do not write a prompt for it either.

Everything is secured through IAM. So there is no API key you have to hand out or rotate. According to the product page, Web Answer stores neither questions nor results and does not use your data for training.

When to use AgentCore Web Answer and when Web Search

The Gateway has offered the Web Search tool since June. The two do not exclude each other; they just divide the work differently. Web Search hands you a list of results, and your own model turns it into an answer. Web Answer hands you the answer directly.

AgentCore Web Answer vs. Web Search: result list for your model on the left, finished answer with sources on the right

Web Search returns results, Web Answer returns a finished answer with sources.

Criterion Web Search Web Answer
Result Up to 25 results with snippet, URL, title, date Finished text with numbered sources
Input Search query up to 200 characters Question up to 30,000 characters
Who summarizes? Your model AWS
Price $7 per 1,000 queries $10 per 1,000 tool calls
Regions including us-east-1, us-west-2, eu-west-1 us-east-1 only
Domain and date filters yes (connector version 1.2.0 or later) yes (version 1.1.0 or later)

My rule of thumb: if your agent should reason about the hits itself, for example to weigh several sources against each other or to combine them with internal data, use Web Search. If you simply want a sub-question answered, say β€œSince when does rule X apply?” in the middle of a longer flow, Web Answer is the leaner choice. In addition, you save the tokens your own model would otherwise spend reading the hits.

If you would rather use web search directly on the model, without a Gateway: I showed that in Bedrock Web Search with a domain filter in TypeScript. That is a third route with its own rules.

How much does AgentCore Web Answer cost?

Billing is per tool call, not per token: $10 for 1,000 calls, so one cent per question. On top of that, the Gateway itself charges $0.005 per 1,000 tool calls, which hardly matters. Lambda and the HTTP API also stay in the cent range for small volumes.

A quick calculation: if your research service answers 1,000 questions per day, you end up at about $10 a day and roughly $300 a month for Web Answer alone. That is not much if the service delivers real value. But it is also not an amount you want to give away through an open endpoint. More on that in the security notes below.

The catches with Region, language and privacy

Before you start, settle three points with your team, because they decide about adoption more than any line of code.

us-east-1 only

At launch, Web Answer is available exclusively in US East (N. Virginia), and the Gateway has to live in the same Region. Internally, the service uses geographic cross-Region inference, meaning model calls within the US. If your questions might contain personal data, for instance because users type freely, take that to your data protection officer first. For general factual questions without personal data, it is usually easier to justify.

English at launch

According to the product page, Web Answer supports English at launch, with more languages announced. For non-English users, this means: either your agent phrases the sub-question in English and translates the answer afterwards, or you test specifically how well other languages already work today. I would not rely on that blindly.

Showing sources is mandatory

The terms of use are clear: whenever you show a result to end users, the sources and their links have to be shown as well. Also, you must not keep results in bulk storage or build your own index from them. A cache that keeps answers for weeks is therefore tricky. That is why the example bakes the source list into the answer instead of making it optional.

Setting up AgentCore Web Answer with the Serverless Framework

Now it gets practical. We build an endpoint POST /ask for a fictional city library. It accepts a question, calls Web Answer only for a few trustworthy reference works and returns the answer and sources as JSON.

🎬 Flow of a question to AgentCore Web Answer – this animated diagram only runs on my site: watch it on sascha-ms.de

Prerequisites

  • An AWS account with permissions for CloudFormation, IAM, Lambda, API Gateway and Bedrock AgentCore in us-east-1
  • Node.js 22 and npm
  • Serverless Framework v4 (npm i -g serverless), which bundles TypeScript on its own
  • The AWS CLI v2 for the check commands

Step 1: Create the project

Create a folder and install the three packages we need to sign the requests. Node.js itself provides the rest, including fetch and the test runner.

mkdir library-web-answer && cd library-web-answer
npm init -y
npm pkg set type=module
npm pkg set scripts.typecheck="tsc -p ." scripts.test="node --import tsx --test test/*.test.ts" scripts.ask="tsx scripts/ask.ts"
npm i @smithy/signature-v4 @aws-crypto/sha256-js @aws-sdk/credential-provider-node
npm i -D typescript tsx @types/node @types/aws-lambda
mkdir src test scripts

The tsconfig.json is deliberately short. We only check types; the Serverless Framework takes care of bundling.

{
  "compilerOptions": {
    "target": "ES2023",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "noEmit": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "types": ["node", "aws-lambda"]
  },
  "include": ["src", "test", "scripts"]
}

Step 2: Gateway, role and target in serverless.yml

The core lives in the resources section. We create three things: an IAM role that allows the Gateway to call Web Answer, the Gateway itself with IAM authentication and the target with the web-answer connector. The domain list sits on the target. Your agent cannot see it and cannot loosen it either.

# serverless.yml
service: library-web-answer
frameworkVersion: '4'

provider:
  name: aws
  runtime: nodejs22.x
  architecture: arm64
  region: us-east-1 # At launch, Web Answer is only available in N. Virginia
  memorySize: 512
  timeout: 28 # HTTP API waits at most 30 seconds
  environment:
    GATEWAY_URL: !GetAtt WebAnswerGateway.GatewayUrl
  iam:
    role:
      statements:
        - Effect: Allow
          Action: bedrock-agentcore:InvokeGateway
          Resource: !GetAtt WebAnswerGateway.GatewayArn

functions:
  ask:
    handler: src/handler.handler
    events:
      - httpApi:
          method: POST
          path: /ask

resources:
  Resources:
    # Role the Gateway uses to call Web Answer
    WebAnswerGatewayRole:
      Type: AWS::IAM::Role
      Properties:
        AssumeRolePolicyDocument:
          Version: '2012-10-17'
          Statement:
            - Effect: Allow
              Principal:
                Service: bedrock-agentcore.amazonaws.com
              Action: sts:AssumeRole
              Condition:
                StringEquals:
                  aws:SourceAccount: !Ref AWS::AccountId
                ArnLike:
                  aws:SourceArn: !Sub arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:gateway/library-web-answer-*
        Policies:
          - PolicyName: invoke-web-answer
            PolicyDocument:
              Version: '2012-10-17'
              Statement:
                - Effect: Allow
                  Action: bedrock-agentcore:InvokeWebAnswer
                  Resource: !Sub arn:aws:bedrock-agentcore:${AWS::Region}:aws:tool/web-answer.v1

    WebAnswerGateway:
      Type: AWS::BedrockAgentCore::Gateway
      Properties:
        Name: library-web-answer
        ProtocolType: MCP
        AuthorizerType: AWS_IAM
        RoleArn: !GetAtt WebAnswerGatewayRole.Arn
        Description: Research assistant for the city library

    WebAnswerTarget:
      Type: AWS::BedrockAgentCore::GatewayTarget
      Properties:
        GatewayIdentifier: !GetAtt WebAnswerGateway.GatewayIdentifier
        Name: answers
        CredentialProviderConfigurations:
          - CredentialProviderType: GATEWAY_IAM_ROLE
        TargetConfiguration:
          Mcp:
            Connector:
              Source:
                ConnectorId: web-answer
              Configurations:
                - Name: WebAnswer
                  ParameterValues:
                    domainFilter:
                      include:
                        - wikipedia.org
                        - loc.gov
                        - britannica.com
                        - gutenberg.org

  Outputs:
    GatewayUrl:
      Value: !GetAtt WebAnswerGateway.GatewayUrl

Two details matter. First, AWS checks the action bedrock-agentcore:InvokeWebAnswer on every request against the fixed ARN …:aws:tool/web-answer.v1, which belongs to AWS, not to you. Second, the Gateway role does not need bedrock:InvokeModel, because Web Answer runs its own model. The condition on aws:SourceArn makes sure that only your Gateway can assume this role.

Step 3: Write the Gateway client with SigV4

Because the Gateway expects IAM, every request has to be signed with Signature Version 4, the signing process used by all AWS APIs. The service name for signing is bedrock-agentcore. The client itself is little more than a signed JSON-RPC call.

// src/gateway-client.ts
// Minimal MCP client for an AgentCore Gateway with IAM authentication (SigV4).
import { SignatureV4 } from '@smithy/signature-v4';
import { Sha256 } from '@aws-crypto/sha256-js';
import { defaultProvider } from '@aws-sdk/credential-provider-node';

export class GatewayClient {
  private nextId = 1;
  private readonly signer: SignatureV4;

  constructor(private readonly gatewayUrl: string, region = process.env.AWS_REGION ?? 'us-east-1') {
    this.signer = new SignatureV4({ service: 'bedrock-agentcore', region, credentials: defaultProvider(), sha256: Sha256 });
  }

  async rpc<T = any>(method: string, params: Record<string, unknown> = {}): Promise<T> {
    const url = new URL(this.gatewayUrl);
    const body = JSON.stringify({ jsonrpc: '2.0', id: this.nextId++, method, params });
    const signed = await this.signer.sign({
      method: 'POST',
      protocol: url.protocol,
      hostname: url.hostname,
      path: url.pathname,
      headers: { host: url.hostname, 'content-type': 'application/json' },
      body,
    });
    const res = await fetch(url, { method: 'POST', headers: signed.headers, body, signal: AbortSignal.timeout(25_000) });
    if (!res.ok) throw new Error(`${method}: HTTP ${res.status} ${await res.text()}`);
    const json: any = await res.json();
    if (json.error) throw new Error(`${method}: ${JSON.stringify(json.error)}`);
    return json.result as T;
  }

  async listTools() {
    const result = await this.rpc<{ tools: { name: string; inputSchema?: any }[] }>('tools/list');
    return result.tools;
  }

  callTool(name: string, args: Record<string, unknown>) {
    return this.rpc('tools/call', { name, arguments: args });
  }
}

The 25-second timeout is no accident. The HTTP API waits at most 30 seconds for Lambda, and Web Answer typically needs a few seconds per answer. If something goes wrong, our function should respond cleanly with an error message instead of being cut off by the API.

Step 4: Parse answers and sources

This is where the real value lies. The answer can consist of several blocks, and each block numbers its sources from 1. A marker [[2](…)] in the second block therefore means the second source of that block, not the second source overall. If you simply concatenate the blocks, you end up showing the wrong evidence. That is why we renumber globally and remove duplicate URLs.

// src/web-answer.ts
// Pure functions around the Web Answer tool: build arguments, find the tool, parse the answer.

export interface AskOptions {
  includeDomains?: string[];
  excludeDomains?: string[];
  from?: string; // YYYY-MM-DD
  to?: string; // YYYY-MM-DD
  country?: string; // ISO 3166-1 alpha-2, e.g. "DE"
}

export interface McpTool {
  name: string;
  inputSchema?: { properties?: Record<string, unknown> };
}

export interface Source {
  number: number;
  url: string;
  domain?: string;
}

export interface WebAnswer {
  text: string;
  sources: Source[];
  latencyMs?: number;
}

const DATE = /^\d{4}-\d{2}-\d{2}$/;
const MAX_DOMAINS = 50;

function checkDomains(label: string, list?: string[]): string[] | undefined {
  if (!list || list.length === 0) return undefined;
  if (list.length > MAX_DOMAINS) throw new Error(`${label}: at most ${MAX_DOMAINS} domains allowed`);
  return list.map((d) => d.trim().toLowerCase());
}

export function buildArguments(question: string, options: AskOptions = {}): Record<string, unknown> {
  const query = question.trim();
  if (query.length === 0 || query.length > 30_000) {
    throw new Error('The question must be between 1 and 30,000 characters long');
  }
  const args: Record<string, unknown> = { query };

  const include = checkDomains('includeDomains', options.includeDomains);
  const exclude = checkDomains('excludeDomains', options.excludeDomains);
  const filters: Record<string, unknown> = {};
  if (include || exclude) {
    filters.domainFilter = { ...(include && { include }), ...(exclude && { exclude }) };
  }

  if (options.from || options.to) {
    for (const d of [options.from, options.to]) {
      if (d && !DATE.test(d)) throw new Error(`Please use YYYY-MM-DD for date ${d}`);
    }
    if (options.from && options.to && options.from > options.to) {
      throw new Error('from is after to');
    }
    filters.publishedDateFilter = {
      ...(options.from && { from: options.from }),
      ...(options.to && { to: options.to }),
    };
  }
  if (Object.keys(filters).length > 0) args.filters = filters;

  if (options.country) {
    if (!/^[A-Za-z]{2}$/.test(options.country)) throw new Error('country needs a country code such as DE');
    args.userLocation = { country: options.country.toUpperCase() };
  }
  return args;
}

// The tool name depends on the target name. The docs show two spellings,
// so we match the end of the name instead of the exact name.
export function findWebAnswerTool(tools: McpTool[]): McpTool {
  const tool = tools.find((t) => t.name.endsWith('WebAnswer'));
  if (!tool) throw new Error(`No Web Answer tool found. Available: ${tools.map((t) => t.name).join(', ')}`);
  return tool;
}

// Connector version 1.0.0 only knows "query". Filters need 1.1.0 or later.
export function supportsFilters(tool: McpTool): boolean {
  return Boolean(tool.inputSchema?.properties && 'filters' in tool.inputSchema.properties);
}

interface RawBlock {
  text: string;
  citations?: { url: string; domain?: string }[];
}

interface RawResult {
  isError?: boolean;
  content?: { type: string; text?: string }[];
  structuredContent?: { answer?: RawBlock[] };
  _meta?: { latencyMs?: number };
}

const MARKER = /\[\[(\d+)\]\(([^)\s]+)\)\]/g;

// Each block numbers its sources from 1. We merge all blocks,
// drop duplicate URLs and renumber the markers globally: [1], [2], ...
export function parseAnswer(result: RawResult): WebAnswer {
  if (result.isError) {
    throw new Error(`Web Answer returned an error: ${result.content?.[0]?.text ?? 'no text'}`);
  }
  const blocks = result.structuredContent?.answer;
  if (!blocks || blocks.length === 0) {
    return { text: result.content?.[0]?.text ?? '', sources: [], latencyMs: result._meta?.latencyMs };
  }

  const sources: Source[] = [];
  const numberFor = (url: string, domain?: string): number => {
    const known = sources.find((s) => s.url === url);
    if (known) return known.number;
    const source = { number: sources.length + 1, url, ...(domain && { domain }) };
    sources.push(source);
    return source.number;
  };

  const parts = blocks.map((block) =>
    block.text.replace(MARKER, (_match, n: string, url: string) => {
      const citation = block.citations?.[Number(n) - 1];
      return `[${numberFor(citation?.url ?? url, citation?.domain)}]`;
    }),
  );

  return { text: parts.join('\n\n'), sources, latencyMs: result._meta?.latencyMs };
}

export function formatWithSources(answer: WebAnswer): string {
  const list = answer.sources.map((s) => `[${s.number}] ${s.url}`).join('\n');
  return list ? `${answer.text}\n\nSources:\n${list}` : answer.text;
}

I am deliberately defensive about the tool name. In one place the AWS docs mention <target>___WebAnswer, in another <target>_WebAnswer_WebAnswer. Instead of relying on one variant, I match the end of the name. Also, connector version 1.0.0 only knows the query field. So I read whether filters are possible from the input schema that tools/list returns.

Step 5: The Lambda handler

The handler ties everything together. It reads the question, limits the sources to the last two years, remembers the tool name per Lambda environment and returns the answer plus sources.

// src/handler.ts
// POST /ask { "question": "..." } -> answer with numbered sources
import type { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from 'aws-lambda';
import { GatewayClient } from './gateway-client';
import { buildArguments, findWebAnswerTool, parseAnswer, supportsFilters, type McpTool } from './web-answer';

const client = new GatewayClient(process.env.GATEWAY_URL!);
let tool: McpTool | undefined; // looked up once per Lambda environment

const json = (statusCode: number, body: unknown): APIGatewayProxyResultV2 => ({
  statusCode,
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify(body),
});

export async function handler(event: APIGatewayProxyEventV2): Promise<APIGatewayProxyResultV2> {
  let question: string;
  try {
    question = JSON.parse(event.body ?? '{}').question ?? '';
  } catch {
    return json(400, { message: 'Body must be JSON' });
  }

  let args: Record<string, unknown>;
  try {
    // Per request: only sources from the last two years (the domain list lives on the target)
    const from = new Date(Date.now() - 2 * 365 * 24 * 3600 * 1000).toISOString().slice(0, 10);
    args = buildArguments(question, { from });
  } catch (err) {
    return json(400, { message: (err as Error).message });
  }

  try {
    tool ??= findWebAnswerTool(await client.listTools());
    if (!supportsFilters(tool)) delete args.filters; // Connector 1.0.0: query only
    const answer = parseAnswer(await client.callTool(tool.name, args));
    console.log(JSON.stringify({ msg: 'web-answer', latencyMs: answer.latencyMs, sources: answer.sources.length }));
    return json(200, answer);
  } catch (err) {
    console.error(err);
    return json(502, { message: 'Web Answer is currently unavailable' });
  }
}

Step 6: Test, deploy, try it out

You can test the pure functions without AWS access. The tests check the arguments, the tool lookup and above all the renumbering of sources.

// test/web-answer.test.ts
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { buildArguments, findWebAnswerTool, parseAnswer, supportsFilters } from '../src/web-answer';

test('builds arguments with filters and country code', () => {
  const args = buildArguments('  What is Lambda SnapStart? ', {
    includeDomains: ['AWS.amazon.com'],
    from: '2026-01-01',
    country: 'de',
  });
  assert.deepEqual(args, {
    query: 'What is Lambda SnapStart?',
    filters: { domainFilter: { include: ['aws.amazon.com'] }, publishedDateFilter: { from: '2026-01-01' } },
    userLocation: { country: 'DE' },
  });
});

test('rejects empty questions and swapped dates', () => {
  assert.throws(() => buildArguments('   '), /between 1 and 30,000/);
  assert.throws(() => buildArguments('x', { from: '2026-05-01', to: '2026-01-01' }), /from is after to/);
  assert.throws(() => buildArguments('x', { from: '01.05.2026' }), /YYYY-MM-DD/);
});

test('finds the tool regardless of the name variant', () => {
  const search = { name: 'x_amz_bedrock_agentcore_search' };
  assert.equal(findWebAnswerTool([search, { name: 'answers___WebAnswer' }]).name, 'answers___WebAnswer');
  assert.equal(findWebAnswerTool([{ name: 'answers_WebAnswer_WebAnswer' }]).name, 'answers_WebAnswer_WebAnswer');
  assert.throws(() => findWebAnswerTool([search]), /No Web Answer tool/);
});

test('detects whether the connector supports filters', () => {
  assert.equal(supportsFilters({ name: 'a', inputSchema: { properties: { query: {}, filters: {} } } }), true);
  assert.equal(supportsFilters({ name: 'a', inputSchema: { properties: { query: {} } } }), false);
});

test('merges blocks and numbers sources globally', () => {
  const answer = parseAnswer({
    isError: false,
    structuredContent: {
      answer: [
        {
          text: 'Libraries lend books [[1](https://a.example/x)] and e-books [[2](https://b.example/y)].',
          citations: [{ url: 'https://a.example/x', domain: 'a.example' }, { url: 'https://b.example/y' }],
        },
        {
          text: 'Fees vary [[1](https://c.example/z)], see also [[2](https://a.example/x)].',
          citations: [{ url: 'https://c.example/z' }, { url: 'https://a.example/x' }],
        },
      ],
    },
    _meta: { latencyMs: 4800 },
  });
  assert.equal(answer.text, 'Libraries lend books [1] and e-books [2].\n\nFees vary [3], see also [1].');
  assert.deepEqual(answer.sources.map((s) => s.url), ['https://a.example/x', 'https://b.example/y', 'https://c.example/z']);
  assert.equal(answer.latencyMs, 4800);
});

test('throws on isError with the tool message', () => {
  assert.throws(
    () => parseAnswer({ isError: true, content: [{ type: 'text', text: 'ValidationException: include lists do not overlap' }] }),
    /include lists do not overlap/,
  );
});
# Tests and type check locally
npm test
npm run typecheck

# Deploy to us-east-1
serverless deploy

# Check the target status (IDs from the AWS console or via list-gateways/list-gateway-targets)
aws bedrock-agentcore-control list-gateways --region us-east-1
aws bedrock-agentcore-control list-gateway-targets --gateway-identifier <GATEWAY_ID> --region us-east-1

# First question through the HTTP API
curl -s -X POST https://<api-id>.execute-api.us-east-1.amazonaws.com/ask \
  -H 'content-type: application/json' \
  -d '{"question":"When did the Library of Congress start digitizing newspapers?"}'

Expected result: npm test reports six passing tests. After the deploy, the output shows the HTTP API URL and, under Stack Outputs, the GatewayUrl. Wait until the target reports the status READY before you ask your first question. For a first test without the HTTP API, a small script that signs directly with your local AWS credentials does the job.

// scripts/ask.ts
// Usage: GATEWAY_URL=... npx tsx scripts/ask.ts "What changed in the latest Node.js LTS release?"
import { GatewayClient } from '../src/gateway-client';
import { buildArguments, findWebAnswerTool, formatWithSources, parseAnswer, supportsFilters } from '../src/web-answer';

const url = process.env.GATEWAY_URL;
if (!url) throw new Error('Please set GATEWAY_URL (output after the deploy)');
const question = process.argv.slice(2).join(' ');

const client = new GatewayClient(url);
const tools = await client.listTools();
const tool = findWebAnswerTool(tools);
console.log(`Tool: ${tool.name} (filters ${supportsFilters(tool) ? 'available' : 'not available'})`);

const started = Date.now();
const answer = parseAnswer(await client.callTool(tool.name, buildArguments(question, { country: 'DE' })));
console.log(formatWithSources(answer));
console.log(`\nTotal time: ${Date.now() - started} ms, of which server-side: ${answer.latencyMs ?? '?'} ms`);

The output then looks like this: first the tool name with a note on whether filters are available, then the answer with markers such as [1] and [2], below it the β€œSources:” list with the URLs and finally the duration. Web Answer reports the server-side time in the _meta.latencyMs field. In the Lambda handler, this time also ends up in the log, so you spot outliers early.

Important for security: In this example, the POST /ask endpoint is public. Every call costs you a cent, and a script with 100,000 requests costs you $1,000. Protect the endpoint before any real use, for example with a JWT authorizer or a Lambda authorizer with API keys, and set up a budget with an alarm in AWS Budgets. To clean up, serverless remove is all you need.

Common mistakes and how to avoid them

β€œNo Web Answer tool found”

Usually the target is not ready yet. Check its status with aws bedrock-agentcore-control get-gateway-target. Only at READY does the tool show up in tools/list. If you enabled semantic search on the Gateway, the list also contains x_amz_bedrock_agentcore_search, often even in first place. So never take the first tool; look it up by name.

ValidationException with domain filters

If you set an include list both on the target and in the request, Web Answer only uses the intersection. If it is empty, the call fails. Request filters can only narrow the list on the target, never extend it. That is intentional, because it prevents a manipulated prompt from unlocking additional domains.

AccessDenied despite a correct policy

There are two typical causes here. Either the Lambda role lacks bedrock-agentcore:InvokeGateway on the Gateway ARN, or the Gateway role lacks InvokeWebAnswer on the tool ARN in us-east-1. And keep the Region in mind: according to the docs, the Gateway has to be in us-east-1, so a Gateway in eu-central-1 cannot use Web Answer as a target.

Timeouts with long questions

Web Answer researches thoroughly, and that takes time. If your users send many questions at once, a synchronous HTTP API is the wrong frame. In that case, accept the job, respond immediately with a job ID and fetch the answer in the background.

My take

I like Web Answer for one reason: it takes a pipeline off teams that almost everyone has built once and hardly anyone enjoys maintaining. Searching, reading, summarizing, mapping sources: none of that is a competitive advantage. One cent per answer is a fair price for it, especially since you save your own model’s tokens.

At the same time, you give up control. You do not see which searches ran, which pages were discarded and which model did the writing. For a library help desk, that is fine. For decisions with real consequences, I would still have a person check the sources, and AWS itself states in the terms of use that Web Answer is no substitute for professional advice.

One point gets skipped too quickly in many discussions: a summarized answer from the web is still third-party content. If your agent processes the answer further and then has tools with write access, the same rules apply as for any other tool result. How I secure such tools is described in Prompt injection in MCP: securing Lambda tools. And if you already offer workflows through the Gateway, you can simply attach Web Answer as another target next to them, as in Step Functions as AI agent tools via AgentCore Gateway.

That leaves the Region. As long as Web Answer only runs in Virginia, many European teams will use it for public factual questions rather than for customer conversations. I am waiting for Frankfurt or Ireland to be added.

FAQ

Is AgentCore Web Answer available in Frankfurt?

No, at launch only in US East (N. Virginia). AWS has announced more Regions but has not named a date. The Web Search tool, on the other hand, is already available in Ireland.

How much does Web Answer cost?

$10 per 1,000 tool calls, so one cent per question. The length of the question or the answer does not matter. The Gateway adds $0.005 per 1,000 calls.

Can I use Web Answer without AgentCore Gateway?

No. Web Answer is built as a Gateway target and is called via MCP. You can, however, reach the Gateway from any program that can send signed HTTP requests, as the Lambda function in this article shows.

Does Web Answer understand questions in other languages?

Officially, Web Answer supports English at launch. For reliable results, have the sub-question phrased in English and translate the answer afterwards.

Your next step

Deploy the example in a sandbox account, ask five questions from your field and actually look at every source. Do the statements match the linked pages? Only then would I decide whether Web Answer belongs in your agents. I am curious: for which sub-questions would you accept a finished answer, and where would you rather see the hits yourself? Let me know in the comments.

Sources

This post was created with AI assistance.

πŸ‘‰ Next step: Not sure which AWS service fits your use case? My AWS service finder asks a few questions and points you to the right one.

πŸ“¬ Stay updated: New articles on serverless, AI and leading tech teams via RSS, browser push or the weekly newsletter (in German).

Originally published on sascha-ms.de. More articles on serverless, AI in everyday development and leading tech teams: sascha-ms.de/en

πŸ“° Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.