Cloudflare.AI reference
CustomTopics
Section titled “CustomTopics”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).
CustomTopics: Managing custom topics
Section titled “CustomTopics: Managing custom topics”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: [],});Dataset
Section titled “Dataset”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.
Dataset: Creating a Dataset
Section titled “Dataset: Creating a Dataset”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"] }, ],});Dataset: Updating a Dataset
Section titled “Dataset: Updating a Dataset”const dataset = yield* Cloudflare.AI.Dataset("SuccessLogs", { gatewayId: gateway.gatewayId, enable: false, filters: [{ key: "success", operator: "eq", value: [true] }],});Evaluation
Section titled “Evaluation”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.
Evaluation: Creating an Evaluation
Section titled “Evaluation: Creating an Evaluation”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),});Gateway
Section titled “Gateway”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.
Gateway: Creating a Gateway
Section titled “Gateway: Creating a 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",});Gateway: Logging
Section titled “Gateway: Logging”const gateway = yield* Cloudflare.AI.Gateway("Gateway", { collectLogs: true, logManagement: 10000, logManagementStrategy: "STOP_INSERTING",});Gateway: Binding into a Worker
Section titled “Gateway: Binding into a Worker”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)),) {}Gateway: Building a LanguageModel
Section titled “Gateway: Building a LanguageModel”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 },});Gateway: Generating Text
Section titled “Gateway: Generating Text”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));Gateway: Streaming Text
Section titled “Gateway: Streaming Text”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", },});Gateway: Tuning the Gateway
Section titled “Gateway: Tuning the Gateway”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,});Gateway: Spend Limits
Section titled “Gateway: Spend Limits”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 ], },});GatewayDynamicRouting
Section titled “GatewayDynamicRouting”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.
GatewayDynamicRouting: Creating a Route
Section titled “GatewayDynamicRouting: Creating a Route”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: {} }, ],});GatewayDynamicRouting: Updating a Route
Section titled “GatewayDynamicRouting: Updating a Route”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: {} }, ],});GatewayProvider
Section titled “GatewayProvider”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.
Model: Selecting a Model
Section titled “Model: Selecting a Model”const model = yield* Cloudflare.AI.Model("Embeddings", { modelName: "@cf/baai/bge-m3",});Model: Subscribing to Batch Events
Section titled “Model: Subscribing to Batch Events”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.
Model: Referencing a Persisted Handle
Section titled “Model: Referencing a Persisted Handle”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.
ProviderKey
Section titled “ProviderKey”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.
ProviderKey: Bringing your own key
Section titled “ProviderKey: Bringing your own key”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"),});QueryGateway
Section titled “QueryGateway”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).
QueryGateway: Calling AI Gateway
Section titled “QueryGateway: Calling AI 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.
QuerySearch
Section titled “QuerySearch”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.
QuerySearch: Querying AI Search
Section titled “QuerySearch: Querying AI Search”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); }),};QuerySearchNamespace
Section titled “QuerySearchNamespace”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); }),};Search
Section titled “Search”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 childApiToken) and anSearchTokenwrapping it (stable childToken), 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 owntokenIdto skip minting and reuse an existing token. - It creates the
SearchInstance(childSearchInstance) 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.
Search: Creating an AI Search pipeline
Section titled “Search: Creating an AI Search pipeline”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 },});Search: Binding to an Effect Worker
Section titled “Search: Binding to an Effect Worker”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)),) {}Search: Binding to an Async Worker
Section titled “Search: Binding to an Async Worker”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.
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.tsimport 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); },};SearchInstance
Section titled “SearchInstance”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.
SearchInstance: Creating a SearchInstance
Section titled “SearchInstance: Creating a SearchInstance”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",});SearchInstance: R2 source options
Section titled “SearchInstance: R2 source options”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 anincludeItemspattern are indexed;excludeItemstakes 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 viarobots.txt) and index the URLs it lists."discover"— start atsourceand 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.tsexport 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 }], }), ); },};SearchNamespace
Section titled “SearchNamespace”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.
SearchNamespace: Creating a Namespace
Section titled “SearchNamespace: Creating a Namespace”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",});SearchNamespace: Updating a Namespace
Section titled “SearchNamespace: Updating a Namespace”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",});SearchNamespace: Grouping pipelines
Section titled “SearchNamespace: Grouping pipelines”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.tsexport 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 }], }), ); },};SearchToken
Section titled “SearchToken”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.
SearchToken: Creating a Token
Section titled “SearchToken: Creating a Token”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,});SecuritySettings
Section titled “SecuritySettings”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).
SecuritySettings: Enabling AI Security
Section titled “SecuritySettings: Enabling AI Security”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,});