Skip to content

Cloudflare.AI reference

Source: src/Cloudflare/AI/CustomTopics.ts

Custom topic categories for AI Security for Apps (Firewall for AI) content detection on a Cloudflare zone (/zones/{zone_id}/ai-security/custom-topics).

The topic list is a zone singleton — it always exists (defaulting to empty) and is never created or deleted, only replaced wholesale via PUT. Reconcile PUTs the desired list when the observed list differs; destroy restores the list the zone had before Alchemy first managed it.

Declare at most one CustomTopics per zone — two instances managing the same zone would fight over the single underlying list.

AI Security for Apps is entitlement-gated: on accounts without the feature every call fails with the typed AiSecurityNotEntitled error (Cloudflare error code 13101).

Classify traffic into two custom topics

const topics = yield* Cloudflare.AI.CustomTopics("Topics", {
zoneId: zone.zoneId,
topics: [
{ label: "billing", topic: "Questions about invoices and payments" },
{ label: "abuse", topic: "Harassment or abusive language" },
],
});

Clear all custom topics

yield* Cloudflare.AI.CustomTopics("Topics", {
zoneId: zone.zoneId,
topics: [],
});

Source: src/Cloudflare/AI/Dataset.ts

A saved log filter (“dataset”) on a Cloudflare.AI. Gateway.

Datasets capture a slice of the gateway’s request logs (filtered by provider, model, success, cost, tokens, etc.) and serve as the input to AI Gateway evaluations. Name, enablement, and filters are all mutable in place; only moving the dataset to a different gateway forces a replacement.

Capture successful requests

const gateway = yield* Cloudflare.AI.Gateway("Gateway");
const dataset = yield* Cloudflare.AI.Dataset("SuccessLogs", {
gatewayId: gateway.gatewayId,
filters: [{ key: "success", operator: "eq", value: [true] }],
});

Capture logs for a specific model

const dataset = yield* Cloudflare.AI.Dataset("LlamaLogs", {
gatewayId: gateway.gatewayId,
name: "llama-traffic",
filters: [
{ key: "provider", operator: "eq", value: ["workers-ai"] },
{ key: "model", operator: "contains", value: ["llama"] },
],
});
const dataset = yield* Cloudflare.AI.Dataset("SuccessLogs", {
gatewayId: gateway.gatewayId,
enable: false,
filters: [{ key: "success", operator: "eq", value: [true] }],
});

Source: src/Cloudflare/AI/Evaluation.ts

An evaluation job on a Cloudflare.AI. Gateway.

Evaluations measure performance (speed, cost, feedback) of the logged traffic captured by one or more datasets on a gateway. They are create-only on Cloudflare’s side: any prop change replaces the evaluation with a fresh job.

const gateway = yield* Cloudflare.AI.Gateway("Gateway");
const dataset = yield* Cloudflare.AI.Dataset("SuccessLogs", {
gatewayId: gateway.gatewayId,
filters: [{ key: "success", operator: "eq", value: [true] }],
});
const types = yield* listEvaluationTypes(gateway.accountId);
const evaluation = yield* Cloudflare.AI.Evaluation("Baseline", {
gatewayId: gateway.gatewayId,
datasetIds: [dataset.datasetId],
evaluationTypeIds: types
.filter((t) => t.mandatory)
.map((t) => t.id),
});

Source: src/Cloudflare/AI/Gateway.ts

A Cloudflare.AI. Gateway for observability, caching, rate limiting, and governance across AI provider requests.

AI Gateway gives your application a stable gateway ID and account-scoped endpoint that can route model requests through Cloudflare. Once bound to a Worker, aiGateway.model({...}) returns an effect/unstable/ai LanguageModel Layer so you use the standard generateText / streamText APIs — provider-agnostic, with caching, rate limiting, retries, and a unified request log handled by the gateway.

Basic gateway

const gateway = yield* Cloudflare.AI.Gateway("Gateway");

Gateway with caching and rate limiting

const gateway = yield* Cloudflare.AI.Gateway("Gateway", {
id: "my-gateway",
cacheTtl: 300,
cacheInvalidateOnUpdate: true,
rateLimitingInterval: 60,
rateLimitingLimit: 100,
rateLimitingTechnique: "sliding",
});
const gateway = yield* Cloudflare.AI.Gateway("Gateway", {
collectLogs: true,
logManagement: 10000,
logManagementStrategy: "STOP_INSERTING",
});

Cloudflare.AI.QueryGateway(gateway) returns a typed, Effect-native client during the Worker’s Init phase. Provide Cloudflare.AI.QueryGatewayBinding once at the bottom of the Init layer chain so every QueryGateway(...) resolves at runtime.

import * as Cloudflare from "alchemy/Cloudflare";
import * as Effect from "effect/Effect";
import { Gateway } from "./Gateway.ts";
export default class Api extends Cloudflare.Worker<Api>()(
"Api",
{ main: import.meta.url },
Effect.gen(function* () {
const aiGateway = yield* Cloudflare.AI.QueryGateway(Gateway);
return {
fetch: Effect.gen(function* () {
// …routes
}),
};
}).pipe(Effect.provide(Cloudflare.AI.QueryGatewayBinding)),
) {}

Call aiGateway.model({...}) with a Workers AI model id. It returns a Layer<LanguageModel, never, RuntimeContext> directly — no API key and no Layer.unwrap, since the binding handles auth and the gateway URL. Build it in the Init phase; construction is pure.

const aiGateway = yield* Cloudflare.AI.QueryGateway(Gateway);
const languageModel = aiGateway.model({
model: "@cf/meta/llama-3.1-8b-instruct",
parameters: { temperature: 0.7, maxTokens: 1024 },
});

Provide the languageModel layer to the handler and call LanguageModel.generateText like any other Effect. Effect.orDie collapses AiError to a defect (a 500); use Effect.catchTag("AiError", …) for typed handling instead.

import { LanguageModel } from "effect/unstable/ai";
import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
fetch: Effect.gen(function* () {
const response = yield* LanguageModel.generateText({
prompt: "Say hello.",
}).pipe(Effect.orDie);
return yield* HttpServerResponse.json({
text: response.text,
usage: {
inputTokens: response.usage.inputTokens.total,
outputTokens: response.usage.outputTokens.total,
},
});
}).pipe(Effect.provide(languageModel));

LanguageModel.streamText returns a Stream of typed response parts. Stream.provide(languageModel) keeps the model available for the whole stream lifetime; pipe through Sse.encode for an SSE response.

import { LanguageModel } from "effect/unstable/ai";
import * as Stream from "effect/Stream";
import * as Sse from "effect/unstable/encoding/Sse";
import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
const stream = LanguageModel.streamText({ prompt }).pipe(
Stream.provide(languageModel),
Sse.encode,
);
return HttpServerResponse.stream(stream, {
headers: {
"content-type": "text/event-stream",
"cache-control": "no-cache",
"x-accel-buffering": "no",
},
});

Every prop maps to an in-place update — no replacement, no downtime.

export const Gateway = Cloudflare.AI.Gateway("Gateway", {
id: "prod-gateway",
cacheTtl: 300,
cacheInvalidateOnUpdate: true,
rateLimitingInterval: 60,
rateLimitingLimit: 100,
rateLimitingTechnique: "sliding",
collectLogs: true,
logManagement: 100_000,
logManagementStrategy: "DELETE_OLDEST",
authentication: true,
});

Per-gateway spend limits replace the deprecated account-level spending limit. Each rule caps cumulative cost (in cents) over a rolling window (in seconds), optionally scoped to specific models or providers.

const gateway = yield* Cloudflare.AI.Gateway("Gateway", {
spendLimits: {
enabled: true,
rules: [
{ limitType: "cost", limit: 500_00, window: "1 day" }, // $500/day
],
},
});

Source: src/Cloudflare/AI/GatewayDynamicRouting.ts

A dynamic routing configuration (“route”) on a Cloudflare.AI. Gateway.

Dynamic routing models request handling as a graph of elements — start, conditional, percentage split, rate limit, model, and end nodes — so a single gateway endpoint can A/B test models, enforce per-user budgets, and fall back between providers without app changes.

Cloudflare versions route configurations: changing elements creates a new version and deploys it; the reconciler also re-deploys when the live deployed version drifts from the desired graph. Renames are applied in place; only moving the route to a different gateway forces a replacement.

const gateway = yield* Cloudflare.AI.Gateway("Gateway");
const route = yield* Cloudflare.AI.GatewayDynamicRouting("Llama", {
gatewayId: gateway.gatewayId,
elements: [
{ id: "start", type: "start", outputs: { next: { elementId: "model" } } },
{
id: "model",
type: "model",
properties: {
provider: "workers-ai",
model: "@cf/meta/llama-3.1-8b-instruct",
retries: 1,
timeout: 30000,
},
outputs: {
success: { elementId: "end" },
fallback: { elementId: "end" },
},
},
{ id: "end", type: "end", outputs: {} },
],
});
const route = yield* Cloudflare.AI.GatewayDynamicRouting("Llama", {
gatewayId: gateway.gatewayId,
elements: [
{ id: "start", type: "start", outputs: { next: { elementId: "model" } } },
{
id: "model",
type: "model",
properties: {
provider: "workers-ai",
model: "@cf/meta/llama-3.3-70b-instruct-fp8-fast",
retries: 2,
timeout: 60000,
},
outputs: {
success: { elementId: "end" },
fallback: { elementId: "end" },
},
},
{ id: "end", type: "end", outputs: {} },
],
});

Source: src/Cloudflare/AI/GatewayProvider.ts

A BYOK (bring-your-own-key) provider credential on a Cloudflare AI Gateway.

Provider configs let the gateway authenticate against upstream model providers (OpenAI, Anthropic, Workers AI, …) with your own API key, stored in Cloudflare Secrets Store. Cloudflare exposes no update API for provider configs, so every prop change replaces the config (the old one is deleted first — a gateway allows only one config per provider slug and alias).

Cloudflare imposes a strict naming contract: the gateway must reference a Secrets Store via its storeId, and the secret must be scoped to ai_gateway and named exactly {gatewayId}_{providerSlug}_{alias}.

GatewayProvider: Creating a Provider Config

Section titled “GatewayProvider: Creating a Provider Config”

Bring your own OpenAI key

const store = yield* Cloudflare.SecretsStore.Store("Store");
const gateway = yield* Cloudflare.AI.Gateway("Gateway", {
id: "my-gateway",
storeId: store.storeId,
});
// The secret name must be `{gatewayId}_{providerSlug}_{alias}`.
// Prefer `Cloudflare.AI.ProviderKey` to wire this secret automatically.
const secret = yield* Cloudflare.SecretsStore.Secret("OpenAiKey", {
store,
name: "my-gateway_openai_default",
value: yield* Config.Redacted("OPENAI_API_KEY"),
scopes: ["ai_gateway"],
});
const byok = yield* Cloudflare.AI.GatewayProvider("OpenAi", {
gatewayId: gateway.gatewayId,
providerSlug: "openai",
alias: "default",
secretId: secret.secretId,
defaultConfig: true,
});

Rate-limit a key

const byok = yield* Cloudflare.AI.GatewayProvider("OpenAi", {
gatewayId: gateway.gatewayId,
providerSlug: "openai",
alias: "default",
secretId: secret.secretId,
rateLimit: 100,
rateLimitPeriod: 60,
});

Source: src/Cloudflare/AI/Model.ts

A persisted, non-owning handle to a Cloudflare Workers AI catalog model.

Deployment validates the model through its schema endpoint and records its name and account in Alchemy state. It does not provision or invoke a model. Destruction removes only the handle from state; Cloudflare owns the model.

const model = yield* Cloudflare.AI.Model("Embeddings", {
modelName: "@cf/baai/bge-m3",
});
const model = yield* Cloudflare.AI.Model("Embeddings", {
modelName: "@cf/baai/bge-m3",
});
const queue = yield* Cloudflare.Queues.Queue("BatchEvents");
yield* Cloudflare.Queues.Subscription("ModelEvents", {
source: model,
events: ["batch.queued", "batch.succeeded", "batch.failed"],
queueId: queue.queueId,
});

These events require asynchronous batch inference. Synchronous inference does not emit batch events.

const model = yield* Cloudflare.AI.Model.ref("Embeddings", {
stack: "models",
stage: "production",
});

The handle must already be deployed. A reference neither invokes the model nor takes ownership of the source stack’s state.

Source: src/Cloudflare/AI/ProviderKey.ts

Declares a Cloudflare AI Gateway BYOK provider key.

Cloudflare requires BYOK secrets to live in the gateway’s attached Secrets Store, be scoped to ai_gateway, and use the exact {gatewayId}_{providerSlug}_{alias} name. This helper keeps that naming contract with the GatewayProvider declaration so app stacks do not have to wire the secret and provider config manually.

The children are namespaced under the given id: a Secret (child Secret) holding the key, and a GatewayProvider (child Provider) referencing it. It returns { secret, gatewayProvider } so either underlying resource stays addressable.

Rotating value updates the secret in place. Changing alias (or providerSlug) renames the secret — a replacement — and cascades: the provider config is replaced and re-pointed at the new secret.

Bring your own OpenAI key

const store = yield* Cloudflare.SecretsStore.Store("Store");
const gateway = yield* Cloudflare.AI.Gateway("Gateway", {
id: "my-gateway",
storeId: store.storeId,
});
const { secret, gatewayProvider } = yield* Cloudflare.AI.ProviderKey("OpenAiKey", {
store,
gatewayId: gateway.gatewayId,
providerSlug: "openai",
value: yield* Config.Redacted("OPENAI_API_KEY"),
});

Multiple keys for one provider

Distinguish keys for the same provider with an alias — each alias gets its own secret and provider config.

const production = yield* Cloudflare.AI.ProviderKey("OpenAiKey", {
store,
gatewayId: gateway.gatewayId,
providerSlug: "openai",
value: yield* Config.Redacted("OPENAI_API_KEY"),
});
const evals = yield* Cloudflare.AI.ProviderKey("OpenAiEvalsKey", {
store,
gatewayId: gateway.gatewayId,
providerSlug: "openai",
alias: "evals",
value: yield* Config.Redacted("OPENAI_EVALS_API_KEY"),
});

Source: src/Cloudflare/AI/QueryGateway.ts

Binding service that turns a Gateway resource into a typed QueryGatewayClient for Worker runtime code. Wraps the Cloudflare.AI. Gateway runtime binding so each operation returns an Effect tagged with GatewayError, exposes the raw Workers AI handle for ai.run(...), and provides a model(options) factory that produces an effect/unstable/ai LanguageModel Layer.

Bind a Gateway to a Worker and obtain the Effect-native AI Gateway client (run, getUrl, model, …).

QueryGateway is a single identifier that is simultaneously the binding’s Context tag, its type, and the callable — yield* Cloudflare.AI.QueryGateway(gateway).

Bind the gateway during the Worker’s init phase, then use run or getUrl from request handlers.

const aiGateway = yield* Cloudflare.AI.QueryGateway(gateway);
return {
fetch: Effect.gen(function* () {
return yield* aiGateway.run({
provider: "workers-ai",
endpoint: "@cf/meta/llama-3.1-8b-instruct",
headers: { "content-type": "application/json" },
query: { prompt: "Write a concise status update" },
});
}),
};

QueryGateway: Driving Effect AI through the gateway

Section titled “QueryGateway: Driving Effect AI through the gateway”

model(options) produces a Layer<LanguageModel, never, RuntimeContext> that translates LanguageModel.generateText / streamText calls (including tool calls and structured outputs) into ai.run(...) against the bound Workers AI model, routed through the gateway.

const aiGateway = yield* Cloudflare.AI.QueryGateway(gateway);
const languageModel = aiGateway.model({
model: "@cf/meta/llama-3.1-8b-instruct",
parameters: { temperature: 0.7, maxTokens: 1024 },
});
const response = yield* LanguageModel.generateText({ prompt }).pipe(
Effect.provide(languageModel),
);

Provide QueryGatewayBinding in the worker’s runtime layer to resolve the underlying Cloudflare.AI. binding at request time.

Source: src/Cloudflare/AI/QuerySearch.ts

Bind a SearchInstance to a Worker and obtain the Effect-native AI Search client (search, chatCompletions, info, stats). The single-instance ai_search binding resolves directly to a runtime SearchInstance.

QuerySearch is a single identifier that is simultaneously the binding’s Context tag, its type, and the callable — yield* Cloudflare.AI.QuerySearch(instance).

Provide QuerySearchBinding in the Worker’s runtime layer.

Bind the instance during the Worker’s init phase, then use search (retrieval only) or chatCompletions (retrieval + generation) from request handlers.

const search = yield* Cloudflare.AI.QuerySearch(instance);
return {
fetch: Effect.gen(function* () {
const answer = yield* search.chatCompletions({
messages: [{ role: "user", content: "How do I deploy?" }],
});
return yield* HttpServerResponse.json(answer);
}),
};

Source: src/Cloudflare/AI/QuerySearchNamespace.ts

Bind a SearchNamespace to a Worker and obtain the Effect-native namespace client whose .get(name) selects an instance at runtime. The ai_search_namespace binding resolves to a runtime SearchNamespace whose .get(name) selects an instance within the namespace at runtime.

QuerySearchNamespace is a single identifier that is simultaneously the binding’s Context tag, its type, and the callable — yield* Cloudflare.AI.QuerySearchNamespace(namespace).

Provide QuerySearchNamespaceBinding in the Worker’s runtime layer.

QuerySearchNamespace: Querying a namespace

Section titled “QuerySearchNamespace: Querying a namespace”
const ns = yield* Cloudflare.AI.QuerySearchNamespace(namespace);
return {
fetch: Effect.gen(function* () {
const answer = yield* ns.get("docs-search").chatCompletions({
messages: [{ role: "user", content: query }],
});
return yield* HttpServerResponse.json(answer);
}),
};

Source: src/Cloudflare/AI/Search.ts

A convenience construct over SearchInstance that auto-creates the sub-resources an AI Search instance typically needs, so a single call wires up a working pipeline. The data source is chosen by what you pass as source — an Bucket for R2, or a URL for a web crawl:

  • For an R2 source, it mints a least-privilege AccountApiToken (AI Search Index Engine, stable child ApiToken) and an SearchToken wrapping it (stable child Token), then passes that token to the instance. Cloudflare requires a service token to read an R2 bucket and only provisions one through the dashboard / Wrangler — never on a programmatic API create — so the construct provisions it for you. Pass your own tokenId to skip minting and reuse an existing token.
  • It creates the SearchInstance (child SearchInstance) with the remaining props.

Drop down to the low-level resources directly when you need to share a token across instances, adopt an existing one, or bind a namespace.

The returned value is an SearchInstance (augmented with the managed serviceToken, undefined for a web crawler), so a Search is usable anywhere a SearchInstance is expected — pass it straight to Cloudflare.AI.QuerySearch(search) or a Worker’s env.

R2-backed instance (token provisioned for you)

Pass an Bucket as source — its presence selects R2.

const bucket = yield* Cloudflare.R2.Bucket("docs");
const search = yield* Cloudflare.AI.Search("docs-search", {
source: bucket,
});

Index only part of a bucket

const search = yield* Cloudflare.AI.Search("docs-search", {
source: bucket,
prefix: "docs/",
include: ["/docs/**"],
exclude: ["/docs/drafts/**"],
});

Reuse an existing service token

const search = yield* Cloudflare.AI.Search("docs-search", {
source: bucket,
tokenId: existingToken.id,
});

Web-crawler source

Pass a URL as source to crawl and index a website (no service token needed). parse.type defaults to "sitemap"; use "discover" to follow links from the seed instead.

const search = yield* Cloudflare.AI.Search("site-search", {
source: "https://example.com",
parse: { type: "discover", contentSelector: [{ path: "/docs", selector: "main" }] },
});

Store crawl output in your own bucket

const store = yield* Cloudflare.R2.Bucket("crawl-store");
const search = yield* Cloudflare.AI.Search("site-search", {
source: "https://example.com",
parse: { type: "discover" },
store: { bucket: store },
});

The returned search is an SearchInstance. Bind it during the Worker’s init phase with Cloudflare.AI.QuerySearch(search), which attaches the single-instance ai_search binding and hands back an Effect-native client whose search / chatCompletions methods return Effects. Provide Cloudflare.AI.QuerySearchBinding in the Worker’s runtime layer.

import * as Cloudflare from "alchemy/Cloudflare";
import * as Effect from "effect/Effect";
import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest";
import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
export default class Api extends Cloudflare.Worker<Api>()(
"api",
{ main: import.meta.url },
Effect.gen(function* () {
const bucket = yield* Cloudflare.R2.Bucket("docs");
const aiSearch = yield* Cloudflare.AI.Search("docs-search", {
source: bucket,
});
const search = yield* Cloudflare.AI.QuerySearch(aiSearch);
return {
fetch: Effect.gen(function* () {
const request = yield* HttpServerRequest;
const query = new URL(request.url).searchParams.get("q") ?? "";
const answer = yield* search.chatCompletions({
messages: [{ role: "user", content: query }],
});
return yield* HttpServerResponse.json(answer);
}),
};
}).pipe(Effect.provide(Cloudflare.AI.QuerySearchBinding)),
) {}

For a vanilla async fetch Worker, pass the search under Worker.env. The engine attaches the same single-instance ai_search binding (see toBinding in WorkerAsyncBindings.ts), orders the deploy bucket → instance → worker, and InferEnv types env.SEARCH as the runtime SearchInstance handle — no hand-written types.

stack.ts
const bucket = yield* Cloudflare.R2.Bucket("docs");
const search = yield* Cloudflare.AI.Search("docs-search", {
source: bucket,
});
export const Api = Cloudflare.Worker("api", {
main: "./worker.ts",
env: { SEARCH: search },
});
export type ApiEnv = Cloudflare.InferEnv<typeof Api>;
// worker.ts
import type { ApiEnv } from "./stack.ts";
export default {
async fetch(request: Request, env: ApiEnv): Promise<Response> {
const query = new URL(request.url).searchParams.get("q") ?? "";
const answer = await env.SEARCH.chatCompletions({
messages: [{ role: "user", content: query }],
});
return Response.json(answer);
},
};

Source: src/Cloudflare/AI/SearchInstance.ts

A Cloudflare.AI. Search (formerly AutoRAG) instance — a fully managed retrieval-augmented generation pipeline over your own data.

An instance continuously indexes a data source (an R2 bucket or a web crawl), embeds it into a managed Vectorize index, and answers search and chat queries against it. Creation returns immediately; the initial indexing run happens asynchronously.

The instance instanceId, namespace, type, source, and embeddingModel are fixed at creation — changing any of them triggers a replacement. Everything else (models, chunking, caching, reranking, public endpoint, sync interval) is mutable in place.

For the common R2 case, prefer the Search construct, which also mints the service token the indexer needs to read your bucket. Use this low-level resource directly when you manage the token yourself, share one token across instances, or group instances under a SearchNamespace.

R2-backed instance

An R2 source needs a service token to read the bucket. Either pass a tokenId (see SearchToken) or let the Search construct provision one for you.

const bucket = yield* Cloudflare.R2.Bucket("docs", {});
const instance = yield* Cloudflare.AI.SearchInstance("docs-search", {
source: bucket.bucketName,
tokenId: serviceToken.id,
});

Tuned retrieval settings

const instance = yield* Cloudflare.AI.SearchInstance("docs-search", {
source: bucket.bucketName,
aiSearchModel: "@cf/meta/llama-3.3-70b-instruct-fp8-fast",
chunkSize: 512,
chunkOverlap: 64,
maxNumResults: 20,
cache: true,
cacheThreshold: "close_enough",
});

For an r2 source, sourceParams filters which objects are indexed (all fields optional):

  • prefix — only index keys under this prefix.
  • includeItems / excludeItems — micromatch glob patterns (* within a path segment, ** across segments; max 10 each). Only objects matching an includeItems pattern are indexed; excludeItems takes precedence.
  • r2Jurisdiction — R2 data-residency jurisdiction of the source bucket.
const instance = yield* Cloudflare.AI.SearchInstance("docs-search", {
source: bucket.bucketName,
tokenId: serviceToken.id,
sourceParams: {
prefix: "docs/",
includeItems: ["/docs/**"],
excludeItems: ["/docs/drafts/**"],
},
});

SearchInstance: Web-crawler source options

Section titled “SearchInstance: Web-crawler source options”

sourceParams.webCrawler tunes how a web-crawler source is fetched, parsed, and stored. All fields are optional.

parseType selects how pages are discovered:

  • "sitemap" (Cloudflare default) — read <seed>/sitemap.xml (discovered via robots.txt) and index the URLs it lists.
  • "discover" — start at source and follow links.

crawlOptions is no longer accepted by the API — Cloudflare removed it; discovery behavior is controlled solely by parseType.

parseOptions controls how each page is parsed:

  • useBrowserRendering — render JS in a headless browser before parsing.
  • includeImages — index image content.
  • specificSitemaps — explicit sitemap URLs to read (for "sitemap").
  • contentSelector{ path, selector }[] CSS selectors scoping which part of a page is indexed per URL path.
  • includeHeaders — extra request headers sent while crawling.

storeOptions overrides where crawled content is stored — Cloudflare provisions managed storage by default:

  • storageId — R2 bucket name to store crawl output in.
  • storageType"r2".
  • r2Jurisdiction — R2 data-residency jurisdiction for the store bucket.

Basic web-crawler instance

const instance = yield* Cloudflare.AI.SearchInstance("site-search", {
type: "web-crawler",
source: "https://example.com",
sourceParams: { webCrawler: { parseType: "discover" } },
});

Fully-configured crawl

const instance = yield* Cloudflare.AI.SearchInstance("site-search", {
type: "web-crawler",
source: "https://example.com",
sourceParams: {
webCrawler: {
parseType: "discover",
parseOptions: {
useBrowserRendering: true,
includeImages: false,
contentSelector: [{ path: "/docs", selector: "main" }],
},
},
},
});

Sitemap source

// Index the URLs listed in one or more sitemaps (the default parse mode).
const fromSitemap = yield* Cloudflare.AI.SearchInstance("sitemap-search", {
type: "web-crawler",
source: "https://example.com",
sourceParams: {
webCrawler: {
parseType: "sitemap",
parseOptions: { specificSitemaps: ["https://example.com/sitemap.xml"] },
},
},
});

Store crawl output in a specific R2 bucket

const instance = yield* Cloudflare.AI.SearchInstance("site-search", {
type: "web-crawler",
source: "https://example.com",
sourceParams: {
webCrawler: {
parseType: "discover",
storeOptions: { storageId: "my-crawl-bucket", storageType: "r2" },
},
},
});

SearchInstance: Grouping under a namespace

Section titled “SearchInstance: Grouping under a namespace”

SearchInstances live in a namespace (the account-provided default when unspecified). Pass a SearchNamespace’s name to group related instances — the engine then orders this instance after the namespace on deploy. The namespace is immutable; changing it replaces the instance.

const ns = yield* Cloudflare.AI.SearchNamespace("docs-ns", {});
const instance = yield* Cloudflare.AI.SearchInstance("docs-search", {
source: bucket.bucketName,
namespace: ns.name,
});

SearchInstance: Binding to an Effect Worker

Section titled “SearchInstance: Binding to an Effect Worker”

Bind the instance during the Worker’s init phase with Cloudflare.AI.QuerySearch(instance), which attaches the single-instance ai_search binding and returns an Effect-native client whose search / chatCompletions methods return Effects. Provide QuerySearchBinding in the Worker’s runtime layer.

import * as Cloudflare from "alchemy/Cloudflare";
import * as Effect from "effect/Effect";
import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest";
import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
export default class Api extends Cloudflare.Worker<Api>()(
"api",
{ main: import.meta.url },
Effect.gen(function* () {
const bucket = yield* Cloudflare.R2.Bucket("docs", {});
const instance = yield* Cloudflare.AI.SearchInstance("docs-search", {
source: bucket.bucketName,
});
const search = yield* Cloudflare.AI.QuerySearch(instance);
return {
fetch: Effect.gen(function* () {
const request = yield* HttpServerRequest;
const query = new URL(request.url).searchParams.get("q") ?? "";
const answer = yield* search.chatCompletions({
messages: [{ role: "user", content: query }],
});
return yield* HttpServerResponse.json(answer);
}),
};
}).pipe(Effect.provide(Cloudflare.AI.QuerySearchBinding)),
) {}

SearchInstance: Binding to an Async Worker

Section titled “SearchInstance: Binding to an Async Worker”

For a vanilla async fetch Worker, pass the instance under Worker.env. The engine attaches the same ai_search binding and InferEnv types env.SEARCH as the runtime SearchInstance handle.

export const Api = Cloudflare.Worker("api", {
main: "./worker.ts",
env: { SEARCH: search },
});
export type ApiEnv = Cloudflare.InferEnv<typeof Api>;
// worker.ts
export default {
async fetch(request: Request, env: ApiEnv): Promise<Response> {
const query = new URL(request.url).searchParams.get("q") ?? "";
return Response.json(
await env.SEARCH.chatCompletions({
messages: [{ role: "user", content: query }],
}),
);
},
};

Source: src/Cloudflare/AI/SearchNamespace.ts

A Cloudflare.AI. Search namespace — a logical grouping for AI Search instances within an account.

Namespaces partition AI Search (formerly AutoRAG) instances: each namespace owns its own set of namespace-scoped instances and can be searched or queried as a unit. The namespace name is its identity — changing it triggers a replacement; only the description is mutable in place.

The account-provided default namespace is reserved: it always exists and Cloudflare disallows modifying or deleting it. Alchemy adopts it so it can be referenced and bound, but never updates or tears it down.

Generated name

const ns = yield* Cloudflare.AI.SearchNamespace("docs", {});

Explicit name and description

const ns = yield* Cloudflare.AI.SearchNamespace("docs", {
name: "docs-search",
description: "Search over the product documentation",
});

Only the description is mutable; changing name replaces the namespace.

const ns = yield* Cloudflare.AI.SearchNamespace("docs", {
name: "docs-search",
description: "Search over docs and changelogs",
});

Group Search pipelines under the namespace by passing the namespace resource itself to each pipeline’s namespace prop. The engine orders each pipeline after the namespace on deploy and tears them down before it on destroy.

const ns = yield* Cloudflare.AI.SearchNamespace("docs", {});
const guides = yield* Cloudflare.AI.Search("guides", {
source: guidesBucket,
namespace: ns,
});
const api = yield* Cloudflare.AI.Search("api", {
source: apiBucket,
namespace: ns,
});

SearchNamespace: Binding to an Effect Worker

Section titled “SearchNamespace: Binding to an Effect Worker”

Bind the namespace with Cloudflare.AI.QuerySearchNamespace(namespace), which attaches the ai_search_namespace binding and returns a client whose .get(name) selects an instance within the namespace at runtime. Provide QuerySearchNamespaceBinding in the Worker’s runtime layer.

import * as Cloudflare from "alchemy/Cloudflare";
import * as Effect from "effect/Effect";
import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest";
import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
export default class Api extends Cloudflare.Worker<Api>()(
"api",
{ main: import.meta.url },
Effect.gen(function* () {
const ns = yield* Cloudflare.AI.QuerySearchNamespace(Docs);
return {
fetch: Effect.gen(function* () {
const url = new URL((yield* HttpServerRequest).url);
const instance = url.searchParams.get("instance") ?? "guides";
const query = url.searchParams.get("q") ?? "";
const answer = yield* ns.get(instance).chatCompletions({
messages: [{ role: "user", content: query }],
});
return yield* HttpServerResponse.json(answer);
}),
};
}).pipe(Effect.provide(Cloudflare.AI.QuerySearchNamespaceBinding)),
) {}

SearchNamespace: Binding to an Async Worker

Section titled “SearchNamespace: Binding to an Async Worker”

For a vanilla async fetch Worker, pass the namespace under Worker.env. InferEnv types env.SEARCH as the runtime SearchNamespace handle.

export const Api = Cloudflare.Worker("api", {
main: "./worker.ts",
env: { SEARCH: namespace },
});
export type ApiEnv = Cloudflare.InferEnv<typeof Api>;
// worker.ts
export default {
async fetch(request: Request, env: ApiEnv): Promise<Response> {
const query = new URL(request.url).searchParams.get("q") ?? "";
return Response.json(
await env.SEARCH.get("guides").chatCompletions({
messages: [{ role: "user", content: query }],
}),
);
},
};

Source: src/Cloudflare/AI/SearchToken.ts

A Cloudflare.AI. Search service token — the credential AI Search uses to access your data source (R2 bucket, Vectorize index, Workers AI) when indexing.

The token wraps an existing Cloudflare API token (cfApiId + cfApiKey). That API token must carry the “AI Search Index Engine” permission group — Cloudflare validates the credential on create and update and rejects tokens without it. Pair it with Cloudflare.ApiToken.AccountApiToken to mint the underlying API token in the same stack, then reference the service token’s id from an AI Search instance’s tokenId prop.

const apiToken = yield* Cloudflare.ApiToken.AccountApiToken("SearchTokenSource", {
policies: [
{
effect: "allow",
permissionGroups: ["AI Search Index Engine"],
resources: { [`com.cloudflare.api.account.${accountId}`]: "*" },
},
],
});
const token = yield* Cloudflare.AI.SearchToken("SearchToken", {
cfApiId: apiToken.tokenId,
cfApiKey: apiToken.value,
});

SearchToken: Using the Token from a SearchInstance

Section titled “SearchToken: Using the Token from a SearchInstance”
const search = yield* Cloudflare.AI.Search("Search", {
source: bucket,
tokenId: token.id,
});

Source: src/Cloudflare/AI/SecuritySettings.ts

AI Security for Apps (Firewall for AI) settings on a Cloudflare zone (/zones/{zone_id}/ai-security/settings).

The settings object is a zone singleton — it always exists and is never created or deleted, only toggled. Reconcile PUTs the desired enabled value when the observed value differs; destroy restores the value the zone had before Alchemy first managed it.

Declare at most one SecuritySettings per zone — two instances managing the same zone would fight over the single underlying setting.

AI Security for Apps is entitlement-gated: on accounts without the feature every call fails with the typed AiSecurityNotEntitled error (Cloudflare error code 13101).

Enable AI Security for Apps on a zone

const securitySettings = yield* Cloudflare.AI.SecuritySettings("AiSecurity", {
zoneId: zone.zoneId,
enabled: true,
});

Pin AI Security off

yield* Cloudflare.AI.SecuritySettings("AiSecurity", {
zoneId: zone.zoneId,
enabled: false,
});