diff --git a/.changeset/typed-fields-renderer.md b/.changeset/typed-fields-renderer.md
new file mode 100644
index 00000000..bd991c15
--- /dev/null
+++ b/.changeset/typed-fields-renderer.md
@@ -0,0 +1,13 @@
+---
+"@cloudhal/astro-renderer": minor
+"@cloudhal/core-ts": minor
+"@cloudhal/cli": minor
+---
+
+Typed custom fields, step 6 (RFC-0041 §9 / ADR-0043): renderer and themes. Additive only: `entry.fields` still means exactly what is stored, and themes copied before this release build and render as before (a frozen 0.4.0 theme joins the compat test).
+
+- **`GraphSource.entryFields(entry, options?)`** on live and snapshot sources (and `entryFields(source, entry, options?)` from `@cloudhal/astro-renderer/fields` for any source): an entry's typed fields as a theme draws them, by key in definition order. Values are resolved by the language rules (a translation's empty localized field reads the original's, `fromOriginal`; non-localized fields come from the translation group's original; language editions own their values), labelled in the page's language and made safe: rich text as HTML from the article body's markdown pipeline (raw HTML escaped, `safeHref` on links), URLs through `safeHref` (dropped when unsafe), images as `` attributes with the asset's alt in the page's language, everything else as formatted text. Also `renderFields`, `hasShownFields`, and `resolveFields`, `formatFieldValue`, `fieldLabel`, `fieldHelp`, `optionLabel` re-exported.
+- **`formatFieldValue(value, def, { locale, defaultLocale?, timeZone?, words? })`** in `@cloudhal/core-ts/fields`: a value as plain text with `Intl` (numbers, dates that never shift a day, instants in their own zone, option labels as a list, yes/no words).
+- **`EntryFields.astro`** in `@cloudhal/astro-renderer/components`: a plain, unstyled `
` of the fields that have a value, with `data-field` / `data-type` hooks for themes; nothing at all when none has one.
+- **Blog starter:** articles of collections with fields show a "Details" list under the cover (English, Turkish and Japanese words). Entries of collections without definitions render byte for byte as before.
+- New guide: `docs/guides/fields.md` (defining fields in Studio, MCP, the API and the CLI; operations; languages; theme API; security).
diff --git a/AGENTS.md b/AGENTS.md
index ce3e5f58..1fca32fa 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -27,9 +27,9 @@ pnpm i18n:export / pnpm i18n:import [--locale xx] # translations as nested ICU
- `packages/core-contracts/src/generated/`, `crates/cloudhal-schema/src/generated/` — generated; never edit.
- `packages/core-contracts/fixtures/{valid,invalid}//` — golden fixtures used by both languages.
- `packages/storage` — `SqlDriver`/`BlobStore` + adapters (`sqlite`, `d1`, `do-sqlite`, `fs`, `r2`; `SqlBlobStore`, `LayeredBlobStore`, `FetcherBlobStore` at the root) + migrations (`migrations/*.ts`, plain SQL). Conformance suites in `src/testing.ts`.
-- `packages/core-ts` — `SiteGraphStore` (CAS on `rev`, snapshots, diff, publish, rollback). Behavioural suite in `src/testing.ts` runs on sqlite and Miniflare D1. `./local` opens the offline store; `seedStarterSite` seeds a demo. `./surfaces` is RFC-0020's generative UI: the catalog (primitives + block types), eval-free surface checks, bindings, repairs, JSON Patch (`patchPage`), `catalogPrompt`; `./interop` converts A2UI v0.9 and json-render to and from surfaces; `./design` is RFC-0021's DESIGN.md lint and guidance. `search.ts` is RFC-0019's FTS5 site search (index written with the publish batch; articles, pages, terms and authors, RFC-0029); `search-provider.ts` is RFC-0029's `SearchProvider` (`fts5`, `memory`, `withFallback`), set with `SiteGraphStoreOptions.search`; `search-ai.ts` is the experimental Cloudflare AI Search provider (`hybridProvider`, `searchProviderFromEnv`, `cloudhal.json` `search`, docs/guides/search.md).
+- `packages/core-ts` — `SiteGraphStore` (CAS on `rev`, snapshots, diff, publish, rollback). Behavioural suite in `src/testing.ts` runs on sqlite and Miniflare D1. `./local` opens the offline store; `seedStarterSite` seeds a demo. `./surfaces` is RFC-0020's generative UI: the catalog (primitives + block types), eval-free surface checks, bindings, repairs, JSON Patch (`patchPage`), `catalogPrompt`; `./interop` converts A2UI v0.9 and json-render to and from surfaces; `./design` is RFC-0021's DESIGN.md lint and guidance. `./fields` is RFC-0041's typed custom fields (definition and value checks, `resolveFields`, `formatFieldValue`, the agents' JSON Schema, the conversion table); `src/fields-operations/` runs rename/retype/localize/remove. `search.ts` is RFC-0019's FTS5 site search (index written with the publish batch; articles, pages, terms and authors, RFC-0029); `search-provider.ts` is RFC-0029's `SearchProvider` (`fts5`, `memory`, `withFallback`), set with `SiteGraphStoreOptions.search`; `search-ai.ts` is the experimental Cloudflare AI Search provider (`hybridProvider`, `searchProviderFromEnv`, `cloudhal.json` `search`, docs/guides/search.md).
- `packages/schedules` — RFC-0008: time model (only `temporal-polyfill/implementation`, `now` injected, never `Temporal.Now`), RRULE subset, release model (`rl1:`), automation/run state, taint and budgets, `runDue`; `./sql` is `SqlScheduleStore`, `./engine` the Temporal-free part core-ts and the site Worker use. Worker-safe; conformance suites in `src/testing.ts` on sqlite and Miniflare D1.
-- `packages/astro-renderer` — `GraphSource` (live/snapshot), `cloudhal()` integration (virtual config + dev route-cache watcher), `.astro` section components (`Surface.astro` for RFC-0020 surfaces; `./surfaces` renders them, `./kit` has the table of contents and reading time). Routes live in starters, not here.
+- `packages/astro-renderer` — `GraphSource` (live/snapshot), `cloudhal()` integration (virtual config + dev route-cache watcher), `.astro` section components (`Surface.astro` for RFC-0020 surfaces; `./surfaces` renders them, `./kit` has the table of contents and reading time; `./fields` and `EntryFields.astro` render typed field values, RFC-0041). Routes live in starters, not here.
- `packages/cli` — the `cloudhal` CLI (`init`, `dev`, `studio`, `mcp`, `deploy`, `push`, `pull`, …; every command is in `src/cli/help.ts`, which `cloudhal --help` prints and the docs site's CLI reference is generated from); TS bin run by Node 24 type stripping. `src/cli.ts` parses flags and dispatches through `COMMANDS`, built from `src/cli/commands-{local,remote,content,ops,site}.ts` (a new command is one handler plus one line in `src/cli/help.ts`). E2E test spawns the bin (Astro disables dev middleware under `VITEST`). Cloudflare tests run both Workers on workerd via wrangler's `createTestHarness`; test files run serially (shared starter caches).
- `packages/cloudflare` — the admin Worker (`createAdminWorker`); `./playground` is RFC-0036's playground Worker (`createPlaygroundWorker`, the `PlaygroundSite` Durable Object, signed session cookies). Generated entries must import `@cloudhal/cloudflare/workerd-prelude` first (ADR-0012).
- `packages/agent-runtime` / `packages/mcp-server` — policy, skills, audit; MCP tools and transports (ADR-0011). `@cloudhal/mcp-server/render` is RFC-0021's `render_page` core: a small DevTools client, the in-page checks and limits; the CLI drives local Chrome with it (`render-chrome.ts`), the admin Worker Browser Rendering (`browser-render.ts`).
diff --git a/apps/docs/src/lib/site-pages.ts b/apps/docs/src/lib/site-pages.ts
index 1ea3875e..36d139fa 100644
--- a/apps/docs/src/lib/site-pages.ts
+++ b/apps/docs/src/lib/site-pages.ts
@@ -86,6 +86,7 @@ const docsIndex: SitePage = {
## Guides
- [Search](/docs/guides/search), [schedules](/docs/guides/schedules), [comments](/docs/guides/comments)
+- [Typed custom fields](/docs/guides/fields): definitions, operations, languages and themes
- [Several sites in one deployment](/docs/guides/multi-site)
- [Analytics and privacy](/docs/guides/analytics-privacy)
- [Translations with Weblate](/docs/guides/translations-weblate)
diff --git a/docs/guides/fields.md b/docs/guides/fields.md
new file mode 100644
index 00000000..928e2509
--- /dev/null
+++ b/docs/guides/fields.md
@@ -0,0 +1,124 @@
+# Typed custom fields: a guide for site owners and theme authors
+
+A collection can declare **typed fields**: an event has a venue, a start time and a ticket link; a job has a deadline and a salary range. Agents fill them through a JSON Schema instead of guessing, people get a form in Studio, and themes can rely on every value being there in the right shape.
+
+The design and its reasons are in [RFC-0041](../rfcs/0041-typed-custom-fields.md) and [ADR-0043](../adr/0043-typed-custom-fields.md).
+
+## What a field is
+
+Each field has a **key** (lower camelCase, like `startsAt`), **labels** per language (with optional help text), a **type**, and a choice of whether it is **localized**. Definitions live in the site's collection settings, in form order, at most 30 per collection and 200 per site. Values live in each entry's `fields` object under the key.
+
+| Type | Value | Shown on the site as |
+|---|---|---|
+| `text` | a string up to `maxLength` (5,000); `multiline` allows line breaks | escaped text |
+| `richText` | Cloudhal Markdown, up to 50,000 characters | HTML from the same Markdown pipeline as article bodies |
+| `number` | a number; optional `integer`, `minimum`, `maximum` | formatted for the reader's language (`1,234.5`, `1.234,5`) |
+| `boolean` | `true` / `false` | the theme's words for yes and no |
+| `date` | a calendar day, `YYYY-MM-DD` (never shifts across time zones) | a long date in the reader's language |
+| `datetime` | `{ "at": "2026-11-05T16:30:00Z", "timeZone": "Europe/Istanbul" }` | the date and time in the value's own zone ("7:30 PM GMT+3") |
+| `url` | `https:`, `http:`, `mailto:`, `tel:`, a site-relative path or a `#fragment` | a link |
+| `image` | an image asset of the same site (`asset_…`) | an `` with the asset's alt text in the reader's language |
+| `select` | one of the field's options, or a list of them with `multiple` | the options' labels in the reader's language |
+
+A field can be **required to publish** (`requiredToPublish`, every type but `boolean`): drafts save without it, but an entry cannot go to review or be published while the value is missing. Text and rich-text fields marked **searchable** add their words to site search from the next publish on (off by default, so internal notes never reach search snippets; see the [search guide](search.md)).
+
+Collections without definitions keep `fields` as free-form JSON, exactly as before. Once a collection has definitions, new keys without a definition are refused (keys stored earlier are kept).
+
+## Defining fields
+
+- **Studio:** open the collection's settings, then **Fields**. Add fields (the key follows the label until you save), reorder them, and edit labels, help, limits and options. Only people who may change site settings can edit fields. Entries of that collection then get a **Fields** card in the editor, one control per type.
+- **Agents (MCP):** `list_collections` returns each collection's `fields` and `fieldsSchema` (the JSON Schema of the values, with labels and help in the caller's language). `update_collection` changes definitions, and `create_entry` / `update_entry` take `fields` (`null` removes a value). Definition changes always wait for a person who may change site settings, in every mode.
+- **API:** `PUT /api/sites/:siteId/collections/:key` with `fields` in the settings; entry routes take `fields` like any other property. Invalid values answer `422 invalid_fields` with one issue per problem (`fields/startsAt/at`).
+- **Files:** definitions travel in the site node, values in entries, so `cloudhal push`/`pull` and archives carry them unchanged.
+
+### Changes that touch values: field operations
+
+Adding fields, reordering, editing labels and help, adding options and toggling `multiline`, `searchable` or `requiredToPublish` are plain edits. A plain edit can never lose a value, so these changes are **operations** instead:
+
+| Change | Studio | CLI | MCP |
+|---|---|---|---|
+| rename a key | the field's menu, **Rename…** | `cloudhal fields rename ` | `rename_field` |
+| change the type | **Change type…** | `cloudhal fields retype [--options a,b] [--multiple] [--clear]` | `change_field_type` |
+| localized on/off | **Languages…** | `cloudhal fields localize on\|off` | `set_field_localized` |
+| remove | **Remove…** | `cloudhal fields remove ` | `remove_field` |
+
+Each runs a **dry run** first (`--dry-run`): how many entries change, and which values move, convert or would be cleared. A type change converts values with a fixed table (text to rich text, number, select or link; any of those back to text; date to datetime at midnight in the site's time zone and back); values that do not convert block it unless you opt in to clearing them (`--clear`, `onInvalid: "clear"`). Every operation takes a snapshot first and can be reverted (`cloudhal fields revert `). A long operation resumes where it stopped (`cloudhal fields continue`). Operations change the draft graph only; publish as usual.
+
+## Languages
+
+Each field decides once whether it is **localized**:
+
+- **Localized** (`localized: true`): every language version of an entry has its own value. When a translation has none, readers see the original's value (Studio shows it as a placeholder; themes get `fromOriginal: true`). Changing a localized value of the original marks its translations as needing an update, like body edits.
+- **Not localized** (`localized: false`): only the translation group's original holds the value, and every translation shows it. A translation cannot store its own different copy (`field_not_localized`). Changing it never makes translations stale. Machine translation never sends these values.
+- **Language editions** (an original in another site of the project): values are copied when the edition is made, and each site owns its own from then on.
+
+Labels, help and option labels are per language too. A site language without its own label uses the site's default language, then any.
+
+## Showing fields in a theme
+
+New sites created with `cloudhal init` show field values on entry pages automatically: the blog starter's article draws a **Details** list under the cover for collections with fields. Entries of collections without definitions render exactly as before. Themes copied before this release keep working unchanged; they simply do not show the values until you add the lines below.
+
+### `EntryFields`: the generic list
+
+```astro
+---
+import { EntryFields } from "@cloudhal/astro-renderer/components";
+// in your entry route, with `source`, `entry`, the page's `media` context and your theme's words
+const fields = await source.entryFields?.(entry, { media, words: { yes: "Yes", no: "No" } });
+---
+{fields && }
+```
+
+It draws a plain definition list of the fields that have a value, in definition order, and nothing at all when none does:
+
+```html
+
+
Venue
Kadıköy Hall
+
Starts
+
+```
+
+It has no styles of its own. Style `.ch-fields`, `.ch-field[data-type="…"]` or one field with `[data-field=""]`. Multi-line text fields carry `data-multiline` (give them `white-space: pre-line`), and values read from the original carry `data-from-original`.
+
+### Your own markup: `entryFields`
+
+`source.entryFields(entry, options)` (or `entryFields(source, entry, options)` from `@cloudhal/astro-renderer/fields`, for any source) returns every defined field by key, in definition order, ready to draw:
+
+| Property | What |
+|---|---|
+| `label`, `help` | in the page's language |
+| `empty` | nothing to show: no value, an unreadable one, an unsafe link or a missing image |
+| `text` | the value as plain text in the page's language; print it escaped |
+| `html` | `richText` only: sanitized HTML (render with `set:html`) |
+| `href` | `url` only: the checked link target |
+| `image` | `image` only: `` attributes (`src`, `srcset`, `sizes`, `width`, `height`, `alt`, …) and `assetId` |
+| `datetime` | `date` and `datetime`: the machine-readable value for `