# Configuration reference

> Everything Heddle knows about your company, and how far it may act, lives in a heddle/ folder in your own site's repository, so every change goes through a pull request you can review. company.json holds the facts it may claim, people.json who approves what, autonomy.json how far each agent may act and site.json how your build is read, among 14 files. heddle validate tells you when something is missing.

Canonical: https://heddle.pro/docs/configuration

---

Four files are required. heddle/company.json, heddle/people.json, heddle/autonomy.json, heddle/personas.json. The rest are optional, and every optional field has a default. heddle validate checks them all.

## heddle/company.json

Company facts, required. The company's facts with their evidence, covering what it sells, where, to whom, and what may and may not be said about it. Every agent and check reads it, and nothing Heddle writes may claim what is not here.

| Field | Type | Required | Default | What it is |
| name | string | yes |  | The company's name as readers see it. Used in titles, llms.txt, JSON-LD, reports and every agent prompt. |
| legalName | string |  |  | The registered legal name, when it differs from name. The brand kit and the Organization markup use it. |
| url | string | yes |  | The live site's address, https://. The audit, the agents and llms.txt read the site from it. |
| kind | one of: service, product, both | yes |  | service, product or both. Decides which page patterns suit the company and how llms.txt lists the offerings. |
| market | object | yes |  | Where the buyers are. Research reads search volume and results pages for this country and language. |
| market.country | string | yes |  | Two-letter country code, such as US. Search data is read for this country. |
| market.language | string | yes |  | Two-letter language code, such as en. |
| market.regions | list of strings |  |  | Regions inside the country the company serves, when it serves only some. |
| description | object | yes |  | How the company describes itself, in two lengths. llms.txt and the agents use them verbatim. |
| description.oneLine | string | yes |  | One sentence: what the company does and for whom. The quote line of llms.txt. |
| description.short | string | yes |  | A 70 to 160 character description, for meta descriptions and directory listings. |
| offerings | list of objects | yes |  | What the company sells, each with the phrases buyers type. Research, strategy and the page planners start from these. |
| offerings[].id | string | yes |  | A short stable id for the offering. |
| offerings[].name | string | yes |  | The offering's name as buyers see it. |
| offerings[].kind | one of: service, product | yes |  | service or product. |
| offerings[].page | string |  |  | The site path that sells this offering. llms.txt lists it under Services or Product. |
| offerings[].buyerPhrases | list of strings | yes |  | What a buyer types the week before they get in touch. |
| offices | list of objects | yes |  | Where the company has offices. llms.txt lists them, and location pages may only claim a presence listed here. |
| offices[].city | string | yes |  | The office's city. |
| offices[].region | string |  |  | The state or region. |
| offices[].country | string | yes |  | The country. |
| offices[].headquarters | boolean |  |  | true for the head office. The headquarters city is pinned into every location family's sibling list. |
| claims | object | yes |  | What may be said with evidence, and wording that is never allowed. The claims check fails a page with a forbidden pattern, and the agents may only state allowed claims. |
| claims.allowed | list of objects | yes |  | Claims the company may make, each with its evidence. |
| claims.forbidden | list of objects | yes |  | Patterns that fail the build wherever they appear, each with the reason. |
| clients | object | yes |  | Which clients may be named, what may be said about each, and engagements that may be described without a name. Agents never name a client not listed here. |
| clients.nameable | list of objects | yes |  | Clients that may be named, with exactly what may be said and the evidence. |
| clients.unnamed | list of objects |  |  | Engagements that may be described but never attributed to a named client. |
| partners | list of objects |  |  | Partner relationships, what may be said about each and what never, with evidence. The claims and voice checks and the writing agents hold copy to it. |
| partners[].evidence | string | yes |  | A URL, a document, or who confirmed it and when. No evidence, no claim. |
| competitors | list of strings |  |  | Competitors' domains. The authority, paid and research agents compare against them; propose --competitors reads what they rank for. |
| search | object |  |  | How the company is read in search tools. |
| search.searchConsoleProperty | string |  |  | The Search Console property, sc-domain:<domain> or a URL prefix. Default: sc-domain: and the site's domain. |
| search.brandPattern | string |  |  | Regex matching branded queries, so they are split from non-brand. |
| search.indexnowKey | string |  |  | An IndexNow key kept with the company's facts. site.json indexing.key is where heddle indexing key writes it. |

## heddle/people.json

People, required. Who approves the agents' work, who writes, who signs outreach and who gets the reports. An agent above report with no approver here only reports.

| Field | Type | Required | Default | What it is |
| approvers | list of objects | yes |  | People who approve agents' pull requests and drafts through signed links, each with the agents they approve for. |
| approvers[].name | string | yes |  | The approver's name. |
| approvers[].email | string | yes |  | Where the approval links are sent. |
| approvers[].approves | list of values | yes |  | Agent ids this person approves. validate fails an agent at propose or merge that nobody approves. |
| authors | list of objects |  |  | People who write under their own name. The author pages, bylines and Person markup come from here. |
| authors[].id | string | yes |  | A short id used in post front matter and the author page's path. |
| authors[].name | string | yes |  | The author's name as it appears in the byline. |
| authors[].role | string | yes |  | The author's role, shown on the author page. |
| authors[].sameAs | list of strings |  |  | The author's own profiles elsewhere (LinkedIn, GitHub), for the Person's sameAs. The author page on the site is the Person's url. |
| outreachSigner | object |  |  | The person whose name signs pitches to publications. Required before the authority agent runs at propose or merge. |
| reportsTo | list of strings | yes |  | Email addresses that get every run's report, the watchdog's alerts and the canary's failures. |

## heddle/autonomy.json

Autonomy and budget, required. How far each agent may act (off, report, propose, merge), its kill switch and schedule, the monthly budget and floors, and where the agents' models and approvals run. playbook/autonomy.md explains each level.

| Field | Type | Required | Default | What it is |
| budget | object | yes |  | Money the agents may spend. They read the remaining credit before each run, stop at the floor and never top up. |
| budget.monthlyUsd | number | yes |  | The monthly budget for model and data spend, all agents together. |
| budget.floorUsd | number | yes |  | Agents stop drafting when the AI Gateway's credit falls under this. Must be below monthlyUsd. |
| budget.dataFloorUsd | number |  |  | Agents skip paid search data when DataForSEO's prepaid balance falls under this, and say so. |
| promoteAfterCleanRuns | integer |  | 5 | How many clean runs in a row an agent configured at merge needs before it merges; until then it proposes. One unclean run resets the count. Default 5. |
| recommendations | object |  |  | The recommendations ledger, heddle/recommendations.json (playbook/recommendations.md) |
| recommendations.maxOpenDays | integer |  | 7 | The watchdog flags a recommendation still open after this many days |
| agents | object | yes |  | One entry per agent in lib/agents/suite.mjs. heddle validate warns when one is missing. |
| runtime | object |  |  | Where the agents' models and approvals run. Only environment variable names live here, never values. |
| runtime.gateway | object |  |  | Cloudflare AI Gateway on unified billing, through which the agents reach Claude. |
| runtime.models | object |  |  | Claude model per role. seo, writer and editor have defaults; any agent id may name its own, and falls back to editor. |
| runtime.approvals | object |  |  | Signed approval links. The secret is shared with the install's Worker (templates/worker). |
| runtime.notify | object |  |  | How reports and approval links reach people. Without a key they are written to the report and the pull request only. |
| runtime.leads | object |  |  | Where the follow-up agent reads new enquiries: the site Worker endpoint and the bearer token variable. |
| runtime.video | object |  |  | The video agent's settings. Secrets are named by variable, never written here. playbook/video.md lists the accounts an install needs. |
| runtime.ideas | object |  |  | Blog ideas by email (playbook/ideas-by-email.md). Authors are people.json authors with an email; only they may file ideas. |
| runtime.gate | object |  |  | The required gates check an agent runs on its own pull request before anything merges (lib/agents/gate.mjs): the build, then heddle check --strict, then a gates commit status. |
| runtime.runner | object |  |  | The Workflow runner (agents.json runner "workflow"): the site Worker starts each slot's job, waits for its signed report and records what it saw. See playbook/agents-runtime.md. |
| publish | object |  |  | Whether agent-sourced posts (the writer's drafts of the content agent's briefs) publish themselves. A post a person asked for (ideas by email) always waits for that person's signed approval, whatever this says. |
| publish.agentSourced | one of: auto, approve |  | "approve" | auto: at level merge, the writer merges a post once every gate passes. approve: an approver merges each one. |
| publish.dailyFloor | integer |  | 0 | Posts the writer aims to publish a day. A day's queue that cannot reach it is reported, so the content agent's briefs are looked at. |
| publish.bylines | list of strings |  |  | people.json author ids (or names) who have agreed that agent-sourced posts may publish under their name without approving each one. Any other auto-published post carries the company's name as its byline. |

## heddle/personas.json

Personas, required. The readers the site is written for. The buyer and helpful gates read each page as its persona (site.json pagePersonas says which), and the writing agents draft for them.

| Field | Type | Required | Default | What it is |
| id | string | yes |  | A short id that site.json pagePersonas refers to. |
| who | string | yes |  | Who this reader is, in a sentence: role, company size, situation. |
| wants | string | yes |  | What they came to the page to get done. |
| knows | string |  |  | What they already know, so pages do not explain it to them. |
| doesNotKnow | string |  |  | What they do not know yet, so pages explain it instead of assuming it. |

## heddle/targets.json

Tracked search targets, optional. The searches the site is tracked on, each with the page meant to win it and its measured demand. heddle research track and the SEO agent read positions for these.

| Field | Type | Required | Default | What it is |
| term | string | yes |  | The search, exactly as people type it. |
| page | string | yes |  | The site path built to win it. A different page of ours ranking instead is reported. |
| location | string |  |  | For place terms, the location to read from, e.g. 'Dallas,Texas,United States'. |
| volume | integer or null | yes |  | null when the tool returned nothing: unknown, never zero. |
| cpc | number or null |  |  | Cost per click when measured, in US dollars; null when unknown. |
| measuredOn | string | yes |  | The day the volume and cost were read, YYYY-MM-DD. |
| unverified | string |  |  | Why the volume is not trusted yet: a close-variant bucket it shares with phrasings that mean something else, or an acronym whose results page is another meaning. The number stays as measured; the note stops it steering. |

## heddle/known-errors.json

Known errors, optional. Mistakes that have shipped before, what is true instead and where each was seen. The known-errors check fails a page that repeats one with a pattern, and the writing agents are given the list so they avoid it.

| Field | Type | Required | Default | What it is |
| id | string | yes |  | A short stable id for the mistake. |
| when | string | yes |  | The situation the mistake turns up in, such as describing partner status. |
| truth | string | yes |  | What is true instead. The agents are told this. |
| pattern | string |  |  | Optional case-insensitive regular expression that finds the mistake. Without one, it is a judgement for a reader and is listed as unchecked. |
| seen | list of strings | yes |  | Where and when it shipped or was caught, as evidence. |

## heddle/topics.json

Content beats, optional. What the content agents watch and write about, with the beats and their feeds, the commercial phrases, what to leave out, the questions buyers ask and the pages to link to.

| Field | Type | Required | Default | What it is |
| beats | list of objects | yes |  | Subjects the content agent watches, each with the feeds it reads and the question that decides whether a story fits. |
| commercialTerms | list of strings |  |  | The company's own commercial phrases. The content agent seeds from them (else from targets.json), and the SEO and strategy agents add them to their plans. |
| exclude | list of strings |  |  | Words that rule a topic out. The content agent drops any suggestion containing one. |
| buyerQuestions | list of objects |  |  | The questions buyers put to answer engines ("best X for Y", "how do I..."), each with the page that answers it. agent-ready fails a page that does not ask the question in a heading or an FAQ, and warns about a question no page answers yet. |
| buyerQuestions[].volume | integer or null |  |  | Monthly searches where measured; null when unknown, never 0 for unknown. |
| buyerQuestions[].source | string |  |  | Where the question came from: a measured search, a prompt seen in an answer engine, a sales call. |
| priorityPages | list of objects |  |  | Pages posts should send in-text links to (the SEO agent's link editor), each with the searches it is built to win. Without it, every page in targets.json is a priority page. |

## heddle/voice.json

Voice, optional. How the company talks and the words it avoids. The voice check fails public pages that use internal language or avoided spellings, and the writing agents are given the same lists.

| Field | Type | Required | Default | What it is |
| internalLanguage | list of objects |  |  | Words the company uses about itself that buyers do not, each with what to say instead. The voice check fails them on public pages. |
| nameAvoid | list of strings |  |  | Wrong spellings of the company's or products' names. The voice check fails them. |
| notes | string |  |  | Free notes on the house voice, for the people and agents who write. |
| doorClosers | list of objects |  |  | Phrases that turn work away, added to the built-in list in lib/checks/voice.mjs. State the preference, never the condition. |
| clientDown | list of strings |  |  | Words that write a client down ("chaos", "nobody owned"), checked on clientRoutes. Replaces the built-in list. |
| clientRoutes | list of strings |  |  | Route regexes for pages that describe a client's work. Default: case studies. |

## heddle/site.json

How Heddle reads this site's build, optional. Every field has a default, so a site that builds to out/ needs no file at all.

| Field | Type | Required | Default | What it is |
| outDir | string |  | "out" | Built HTML, relative to the site repository. |
| contentDirs | list of strings |  | [] | Markdown and MDX sources, relative to the site repository. Prose checks read these as well as the built pages. |
| contentRoutes | list of strings |  | [] | Route regexes for pages built from contentDirs. Prose checks judge those once, as source, and read every other built page as HTML. |
| pagesDir | string |  |  | Where the writer puts a landing page a person asked for (an idea whose brief kind is "page"), relative to the site repository. Kept out of contentDirs: these are pages at the site root, not posts. |
| proseExempt | list of arrays |  |  | [route, reason] pairs for pages whose text nobody here may rewrite (legal terms). Left out of every prose check. |
| counts | object |  |  | For a site that describes Heddle itself. The counts check fails a page or company.json claim that states a count of Heddle's checks, page families or agents other than the catalog's. |
| counts.catalog | string | yes |  | A JSON file, relative to the site repository, with a counts object written from Heddle's lib/counts.mjs machineryCounts(). |
| counts.except | list of strings |  |  | Phrases that make a match acceptable when they appear in the same sentence, for a sentence counting something else with the same noun. |
| baseUrl | string |  |  | Defaults to company.json url. |
| model | object |  |  | Where the judging model runs. Only the environment variable names live here, never values. |
| model.gatewayEnv | string |  |  | Names the AI Gateway, for agents that bill through one. The checks call Workers AI directly. |
| pagePersonas | list of objects |  |  | Which reader each page is written for, first match wins. Without it, the first persona in personas.json reads every page. |
| pagePersonas[].match | string | yes |  | Regular expression over the route. |
| pagePersonas[].persona | string or null | yes |  | A personas.json id, or null to exempt the page (then why is required). |
| pagePersonas[].sells | boolean |  | true | false: the page is there to be believed, not to ask for the call, so the buyer test skips 'talk'. |
| pagePersonas[].kind | one of: sales, reference |  | "sales" | reference: the page answers a search (a comparison, a definition, a catalog entry) rather than selling. The buyer test holds it only to the internal-language question; the answers check judges whether it answers its search. |
| meta | object |  |  | Options for the meta check: titles, descriptions, canonicals, breadcrumbs and Markdown twins on the built pages. |
| meta.exempt | list of arrays |  |  | [route, reason] pairs. |
| meta.requireMarkdownTwin | boolean |  | false | Fail a page with no <route>.md beside it, for sites that publish Markdown mirrors for answer engines. |
| meta.runtimePaths | list of strings |  |  | Path prefixes served at runtime rather than from the build, so an og:image there is not looked for on disk. |
| meta.thinWords | integer |  | 120 | A page whose body (its <main>, or its one <article>) has fewer words than this is thin: the meta check warns unless it carries noindex. |
| meta.titleSuffix | string |  |  | The brand suffix the layout adds to titles (" · Brand"). With it set, a route matching bareTitles fails if its title ends with it: a post spends every character on its topic. |
| meta.bareTitles | list of strings |  |  | Route regexes whose titles never carry titleSuffix. Default: surface.posts. |
| density | object |  |  | Options for the density check, which fails long paragraphs outside articles. |
| density.articles | list of strings |  |  | Route regexes for authored articles, exempt from the paragraph-length rule. Default: blog, case studies, legal pages. |
| humanizer | object |  |  | Options for the humanizer check: rules the house style does not use, exempt pages, and names it must not flag. |
| humanizer.disable | list of strings |  |  | Rule ids this company's house style does not use, e.g. em-dash. |
| humanizer.exempt | list of arrays |  |  | [file or route, reason]: whole-document rules off, for generated reference pages. |
| humanizer.names | list of strings |  |  | Product and company names that match a banned word ("Profound"); matched case-sensitively and skipped. |
| answers | object |  |  | Options for the answers gate, which asks the model whether openings and FAQ answers answer their question. |
| answers.gate | boolean |  | false | false reports weak openings without failing, as on the source site. |
| searchSurface | object |  |  | Options for the search-surface check, which holds titles, H1s and canonicals to a recorded surface. |
| searchSurface.record | string |  | "search-surface.json" | The recorded surface, relative to heddle/. Rewritten by heddle check --only search-surface --update. |
| searchSurface.ownerPattern | string |  |  | Regular expression over a title whose groups 1 and 2 are the service and the place the page owns. Default: "<Service> in <Place>, ST". |
| urls | object |  |  | Options for the urls check: the legacy URLs that must still resolve and the redirects the server makes. |
| urls.legacy | string |  | "heddle/legacy-urls.txt" | Every path the site has been indexed at, one per line, relative to the site repository. A floor, not a census: add paths found in Search Console by hand. |
| urls.redirects | list of strings |  |  | Paths the server redirects: exact, or a prefix ending in /*. |
| urls.unverified | list of strings |  |  | Path prefixes served at runtime rather than from the build, so a link into them is not called dead. |
| orphans | object |  |  | Options for the orphans check: pages the navigation must link, exemptions and inbound-link floors per family. |
| orphans.navRequired | list of strings |  |  | Route regexes for pages the navigation must link to, e.g. every service page. |
| orphans.exempt | list of arrays |  |  | [route, reason] pairs for pages meant to be reached only from outside, such as a campaign landing page. |
| orphans.familyFloors | object |  |  | Per page family, the fewest inbound links its worst-linked page and its median page should have. Under either warns. |
| brand | object |  |  | Where heddle brand writes the kit and where the site serves it. |
| brand.kitDir | string |  | "public/brand/kit" | Relative to the site repository. |
| brand.page | string |  | "/brand" | The brand page's route. |
| brand.stem | string |  |  | Prefix for every kit file name. Defaults to the brand name, lower case and hyphenated. |
| playbook | object |  |  | Publish Heddle's playbook on this site (lib/playbook/pages.mjs): heddle generate writes heddle/generated/playbook.json for the pages and the public Agent Skill under the route; heddle aeo lists the skill in llms.txt. templates/next/playbook/ renders it. |
| playbook.publicDir | string |  | "public" | The folder the site serves as-is; the skill goes in <publicDir><route>/. |
| playbook.checks | string |  |  | Where this site publishes Heddle's checks, one page per check id; the checklist links each check there. Leave out when it does not. |
| playbook.patterns | string |  |  | Where this site lists the page families, when it does. |
| playbook.agents | string |  |  | Where this site describes the agents, when it does. |
| playbook.pages | object |  |  | Copy overrides per page, measured titles above all. {tactics}, {lessons}, {enforced}, {sources}, {families}, {checks} and {agents} are filled with the counts. |
| docs | object |  |  | Publish Heddle's documentation on this site (lib/docs/pages.mjs): heddle generate writes heddle/generated/docs.json with the getting-started, install, command, agents, configuration and troubleshooting pages, all generated from Heddle. templates/next/docs/ renders it. |
| docs.enabled | boolean |  | true | false keeps the block without publishing. |
| docs.route | string |  | "/docs" | Where the documentation lives on the site; each page is a path under it. |
| docs.pages | object |  |  | Per page (hub, getting-started, install, commands, agents, configuration, troubleshooting), copy that replaces the default: title, h1, description, answer, faqs. {commands}, {agents} and the other counts are filled in. |
| aeo | object |  |  | Options for heddle aeo: what the Markdown twins leave out, the twin size floor, the _headers file and extra llms.txt sections. |
| aeo.chromeClasses | list of strings |  |  | CSS classes on kickers, chips and labels that are page furniture, left out of the Markdown twins. |
| aeo.exclude | list of strings |  |  | Route regexes that get no twin. |
| aeo.minBytes | integer |  | 400 | A twin shorter than this means the extraction missed the page; nothing is written. |
| aeo.requireQuestions | boolean |  | false | Fail a page whose twin has no questions section. |
| aeo.headers | boolean |  | false | Also write _headers (Cloudflare and Netlify format) with a Link header to each page's twin. |
| aeo.headLinks | boolean |  | true | Write <link rel="alternate" type="text/markdown"> into each page's head, announcing its twin to a crawler that reads HTML and never headers. |
| aeo.llmsSections | list of objects |  |  | Extra sections for llms.txt, for files that are not pages (a published Agent Skill, an API spec). Each link's url is absolute or a path on the site, and a path must exist in the build. |
| agentReady | object |  |  | The agent-ready check: whether an assistant can read the site without a browser. |
| agentReady.crawlers | list of strings |  |  | User agents robots.txt must let fetch /. Defaults to GPTBot, OAI-SearchBot, ChatGPT-User, ClaudeBot, Claude-SearchBot, PerplexityBot, Google-Extended. |
| agentReady.answerMin | integer |  | 40 | Fewest words in the paragraph under the H1. |
| agentReady.answerMax | integer |  | 80 | Most words in the paragraph under the H1. |
| agentReady.minWords | integer |  | 50 | Fewer words than this in the HTML's main content means it is rendered by JavaScript. |
| agentReady.linkHeaders | one of: headers, runtime |  | "headers" | Where the Link header announcing each Markdown twin comes from: one _headers rule a page (headers), or the Worker at runtime (runtime: heddle aeo writes heddle-twins.json for templates/worker/heddle-twins.ts). Cloudflare reads 100 _headers rules, so a site past about 80 pages needs runtime. |
| agentReady.exempt | list of arrays |  |  | [route, reason] pairs for pages with nothing to offer an assistant. |
| agentReady.blocked | list of arrays |  |  | [crawler, reason] pairs: answer-engine or training crawlers robots.txt blocks on purpose. Blocking one is a business decision, allowed when written down. |
| legacy | object |  |  | heddle legacy: where the inventory of every URL the site was ever indexed at comes from. |
| legacy.from | string |  |  | The live site whose sitemap is read. Defaults to company.json url. |
| legacy.gscDays | integer |  | 480 | Days of Search Console pages to read when credentials exist. |
| launch | object |  |  | Settings for heddle launch (playbook/launch.md). Every field has a default read from the site. |
| launch.worker | string |  |  | The Worker config, relative to the site. Default wrangler.jsonc. |
| launch.repo | string |  |  | GitHub Owner/name. Default: the origin remote. |
| covers | object |  |  | Where the covers agent reads posts and writes covers (lib/covers/site.mjs). |
| covers.posts | string |  |  | The folder of Markdown posts. Default: the first contentDirs entry. |
| covers.dir | string |  | "public/covers" | Where <slug>.svg and the 1200x630 <slug>-og.png are written. |
| covers.publicPath | string |  | "/covers" | The URL the dir is served at. |
| covers.shareField | string |  |  | A frontmatter field the site reads as the post's share image (e.g. "image"). When set, the proposal writes it into the post. |
| covers.fallback | object |  |  | The deterministic fallback cover (heddle covers --fallback). |
| helpful | object |  |  | The helpful-content gate (lib/checks/helpful.mjs). Floors default to the source site's measured ones. |
| helpful.articles | list of strings |  |  | Route patterns judged as articles (teach or show work); the rest are pages. |
| helpful.exempt | list of strings |  |  | Route patterns not judged (legal, forms, listings). proseExempt routes are exempt too. |
| helpful.tolerance | number |  | 0.12 | How far under its recorded score a baselined page may read. Capped at 0.13, and never more than half the recorded score. |
| helpful.minReads | integer |  | 1 | Readings a page gets the first time it is read, before the mean counts. |
| helpful.base | string |  |  | The git ref strict mode compares the record with when neither --base nor the pull request names one. Default origin/main. |
| helpful.labels | object |  |  | Pull-request labels for the two exceptions to the record check. Read from the base branch. |
| helpful.heldRoutes | list of strings |  |  | Route patterns a held-content pull request may excuse. Default: the article routes. |
| helpful.nearCopy | object |  |  | Near-copy between pages of one family (lib/checks/near-copy.mjs). |
| helpful.sources | list of objects |  |  | How a route maps to its own source files, for the held exception: a route pattern and a path where $1.. are its groups. Default: the route's path under each contentDir. Read from the base branch. |
| indexing | object |  |  | Getting changed pages crawled (lib/indexing): IndexNow and the Search Console indexing request a computer-use bot performs. |
| indexing.key | string |  |  | The IndexNow key. Public by design: it only lets this host ask for a recrawl. heddle indexing key creates it and writes <key>.txt into publicDir. |
| surface | object |  |  | The surface check (lib/checks/surface.mjs, playbook/seo-aeo-geo.md). |
| surface.posts | list of strings |  |  | Route patterns for posts, which need BlogPosting, dates, a card of their own and a visible date. Default ["^/blog/[^/]+$"]. |
| surface.allowRatings | boolean |  |  | Only when real reviews or ratings are shown on the page they are marked up on. |
| surface.timezone | string |  |  | The site's IANA time zone (America/Phoenix). Every date with a time must carry the offset this zone has on that day. |
| surface.launched | string |  |  | The launch day. A build on that day may date every sitemap URL with it; on any other day, every lastmod equal to the build date fails. |
| surface.feed | string |  |  | The feed's path in the build, when it is not one of rss.xml, feed.xml, atom.xml, index.xml or blog/rss.xml. |
| checks | object |  |  | Which opt-in checks run on every build for this install. |
| checks.include | list of values |  |  | Opt-in checks (they fetch the pages a post cites) that heddle check runs on every build for this install, not only with --only. |
| restatement | object |  |  | Limits for the restatement check (lib/checks/restatement.mjs). Default: the source site's calibration, the highest an approved explainer reached plus half again. |
| outbound | object |  |  | The opt-in outbound check (heddle check --only outbound): every link to another site resolves. |
| outbound.skip | list of arrays |  |  | [host, reason] pairs never fetched (a host that bans scripts). |
| outbound.pauseMs | integer |  | 2000 | Wait before the second, slower attempt. |
| diagrams | object |  |  | Mermaid diagrams (heddle diagrams, lib/diagrams, the diagrams check). |
| diagrams.dir | string |  | "content/diagrams" | Where rendered SVGs are committed, <key>.svg, plus index.json (key -> caption and source). |
| diagrams.sources | list of strings |  |  | Folders or files scanned for ```mermaid blocks, Markdown and JSON. Default: contentDirs plus heddle/families. |
| diagrams.since | string |  |  | Posts dated on or after this day may not embed an SVG image or raw <svg>. Default: every post. |
| diagrams.colors | object |  |  | Colour role -> brand hex. Default: read from brand.json tokens. |
| diagrams.font | string |  |  | The label face. Default: brand.json's Body type family. |
| diagrams.fontStack | string |  |  | The font-family the inlined SVG asks the page for, e.g. "var(--font-inter), Inter, sans-serif" when next/font hashes the family name. |
| diagrams.fonts | list of objects |  |  | Faces the renderer loads before measuring labels. |
| diagrams.require | one of: either, diagram, image, both, off |  | "either" | What every post must carry in its body. On by default: a post with no diagram and no image fails. |
| diagrams.requireSince | string |  |  | Apply require only to posts dated on or after this day (for an install with an archive). |
| images | object |  |  | The images check and heddle images. |
| images.publicDir | string |  | "public" | Where site-relative image paths are found on disk. |
| assets | object |  |  | The assets check: hosts a page may load from. |
| assets.allow | list of arrays |  |  | [host, reason] pairs a page may load assets from. |
| assets.banned | list of arrays |  |  | [pattern, reason] pairs for hosts known dead or leaving; failed even as a link. |
| media | object |  |  | Binary media in object storage (heddle media, lib/media). Only for media too large or too many for git. |
| media.dir | string |  | "public/media" | Local media, relative to the site repository. Not a mirror of the bucket. |
| media.bucket | string | yes |  | The R2 bucket name. |
| media.prefix | string |  | "media" | Key prefix in the bucket, and the URL path the Worker serves it at. |
| media.cacheHeader | string |  |  | A response header that only the bucket path sets (e.g. x-media-cache), so --headers can tell an R2 answer from a bundled fallback. |
| data | object |  |  | Where the agents' search data comes from (lib/data). Agents ask for a capability (keyword volume, positions, backlinks, Core Web Vitals...); the providers named here answer. Only environment variable names live here, never values. Without this block an install keeps DataForSEO, Search Console and anonymous PageSpeed at their default variable names. |
| data.prefer | object |  |  | Per capability, the providers to try in order, e.g. { "rankTracking": ["openseo", "dataforseo"] }. |
| contact | object |  |  | The contact form's endpoint in the site Worker (templates/worker/heddle-contact.ts). Every field has a default. |
| contact.path | string |  | "/api/contact" | Where the form posts. |
| contact.defaultTopic | string |  | "Enquiry" | The topic when the form sends none. |
| contact.needMore | string |  |  | The error shown when neither a site nor a message of minMessage characters was given. |
| contact.accent | string |  |  | The internal email's accent colour; brand.json's --spot colour when unset. |
| worker | object |  |  | Optional routes of the site Worker (templates/worker/heddle.ts). |
| worker.tools | boolean |  | false | Mount the free tools' fetch route, POST /api/tools/fetch (templates/worker/heddle-tools.ts). |

## heddle/brand.json

Brand, optional. Everything the /brand page shows and the downloadable kit carries, written once. heddle brand builds the kit from it; the site's brand page renders from it.

| Field | Type | Required | Default | What it is |
| name | string |  |  | Defaults to company.json name. Set it when the brand is a product of the company, not the company. |
| legalName | string |  |  | Who owns the mark. Defaults to company.json legalName. |
| founded | integer |  |  | The year the company was founded, shown on the brand page and in the kit's manifest. |
| contact | string | yes |  | Where to ask for permission or a file that is not in the kit. |
| press | object |  |  | Optional. A named press contact, when it is not the general contact. |
| permissions | string |  |  | What may be done with the files without asking, and what needs a conversation. Defaults to a neutral statement naming the contact. |
| tagline | string |  |  | The brand line, if there is one. Not a description: the description is boilerplate. |
| boilerplate | object | yes |  | The canonical descriptions of the company, in a long and a short form, for press and directories. |
| boilerplate.long | string | yes |  | The canonical paragraph, identical on GitHub, npm, LinkedIn, directories and the site's metadata. |
| boilerplate.short | string | yes |  | Must equal company.json description.short. |
| boilerplate.oneLine | string |  |  | Must equal company.json description.oneLine when both are set. |
| naming | object | yes |  | How the name is written, and spellings to avoid. |
| naming.avoid | list of strings | yes |  | Spellings a reader will meet that are wrong. |
| naming.note | string |  |  | How to write the name, in a sentence. |
| partnerships | object |  |  | Approved wording for a partnership, and the words that are banned. Only when there is one. |
| logo | object | yes |  | The wordmark and mark files, clear space, minimum size and what never to do with them. |
| logo.description | string |  |  | What the logo is, in a sentence or two, for the page. |
| logo.donts | list of strings | yes |  | Short 'please do not' lines shown beside the logo. |
| logo.pngWidth | integer |  | 2048 | Width of the wordmark PNGs. Marks are rendered 1024 high. |
| background | object | yes |  | The hero image the site ships, and how to put text on it. |
| background.file | any | yes |  | The hero image the site already ships. |
| background.use | string | yes |  | How to put text on it. |
| banners | object |  |  | Banner and share images for the kit: reuse a file the build makes, or draw one. |
| banners.cover | boolean |  | true | Also compose a 1584x396 LinkedIn cover. |
| colors | list of objects | yes |  | The brand colours, each with its hex value and what it is used for. Diagrams and covers draw only in these. |
| colors[].use | string | yes |  | Its one job. Where two similar colours exist, say how their jobs differ. |
| colors[].token | string |  |  | The CSS custom property that carries it on the site, if any. |
| type | list of objects | yes |  | The typefaces, each with its role, weight, source and a sample. |
| type[].stack | string |  |  | The CSS font stack, when the family is a system stack rather than one face. |
| type[].source | any | yes |  | Where to get it. |
| voice | list of objects | yes |  | Three to six do and don't pairs on how the brand writes, shown on the brand page. |
| family | list of objects |  |  | Sibling products that share the mark, if any. |
| family[].script | string or null |  |  | The name as set in a lockup script, or null for the parent brand. |
| lockups | string |  |  | How a family product's lockup is built. |
| rules | list of strings | yes |  | Eight at most, verbatim on the page, in the manifest and in the README. |
| facts | list of objects |  |  | The facts table for a press mention: licence, website, source, and so on. |
| faqs | list of objects |  |  | Questions and answers shown on the brand page, with FAQPage markup. |
| copy | object |  |  | Optional headings for the page's sections. The reference page has neutral defaults for every one. |

## heddle/agents.json

Agent manifest, optional. What each agent must produce and by when, the failures it can emit, and the smoke checks a deploy must pass. Optional. An install without heddle/agents.json is held to the defaults in lib/agents/manifest.mjs. See playbook/reliability.md.

| Field | Type | Required | Default | What it is |
| owner | string | yes |  | The email of the person who answers for the agents. Gets the daily digest and every alert. |
| notify | list of strings |  |  | Others who get alerts. people.json reportsTo is always added. |
| systemic | object |  |  | When repeated failures become one systemic alert. |
| systemic.perAgent | integer |  | 2 | The same normalised reason on this many attempts or items of one agent inside the window. |
| systemic.acrossAgents | integer |  | 3 | The same normalised reason this many times across two or more agents inside the window. |
| digest | object |  |  | The watchdog's daily digest email: when it is sent and what its first line counts. |
| digest.hourUtc | integer |  | 13 | The first watchdog run at or after this hour sends the day's digest. |
| digest.headline | list of objects |  |  | The counts in the digest's first line. Defaults to posts published, drafts waiting and ideas filed. |
| deploy | object |  |  | What the post-deploy smoke and the config-change check hold the site Worker to. |
| deploy.config | string |  | "wrangler.jsonc" | The site Worker's wrangler config, relative to the repository root. |
| deploy.previewRequired | boolean |  | true | An upload must print a preview URL. True whenever an agent drafts against previews. |
| agents | object | yes |  | One entry per agent. An agent left out keeps its default entry. |
| approvals | one of: gated, ungated |  | "gated" | The install's approval setting. gated: a named person approves each post before it publishes, so a draft waiting for them is an acceptable end state. ungated: agents publish once every check passes and revise against failed checks, so only published or declined ends an item. See playbook/reliability.md. |

## heddle/install.json

What heddle install put into this site, optional. json, written by heddle install and heddle update and read by heddle doctor. Not edited by hand.

| Field | Type | Required | Default | What it is |
| heddle | string | yes |  | The Heddle version that wrote it. |
| features | list of values | yes |  | The features installed: worker, next, agents, deploy, config-check, ideas, approvals, smoke. heddle update and doctor read them. |
| files | object | yes |  | Each managed file, by its path in the site: the template it came from and the SHA-256 of the template's text as installed (header excluded), so doctor can tell a stale copy from an edited one. |

## heddle/recommendations.json

Recommendations ledger, optional. Every recommendation an agent made and what became of it. Written by the agents (lib/agents/recommendations.mjs) and by people closing entries by hand. playbook/recommendations.md says how.

| Field | Type | Required | Default | What it is |
| id | string | yes |  | <agent>-<hash of the agent's key>; stable, so the same finding is one entry |
| agent | string | yes |  | The agent that raised it, or "person" for one a person added |
| date | string | yes |  | The day it was first raised |
| recommendation | string | yes |  | What to do, in one sentence. |
| evidence | string or object or array or null |  |  | What the agent measured or read: a string, or the agent's own record |
| source | one of: agent, proposal, person |  |  | agent: raised in result.recommendations; proposal: a draft the agent produced; person: added by hand |
| status | one of: open, implemented, rejected, needs-person | yes |  | open, implemented, rejected or needs-person. implemented needs a pr, commit or verifiedBy; rejected needs a reason; needs-person needs the step. |
| pr | string |  |  | The pull request that implements it, or that a person must review |
| commit | string |  |  | The commit that implemented it. |
| verifiedBy | string |  |  | The run that re-read the finding and no longer found it |
| recheck | string |  |  | The agent's check that re-reads this finding every run |
| reason | string |  |  | Why it was rejected |
| step | string |  |  | Exactly what a person must do |
| lastSeen | string |  |  | The last day a run raised it again. |
| updated | string |  |  | The last day the entry changed. |
| closedOn | string |  |  | The day it left open. |

## heddle/families/<family>.json

Family dataset, optional. The rows of one family, which are the data each of its programmatic pages is built from. heddle generate writes the publishable rows and their sibling links into heddle/generated/.

| Field | Type | Required | Default | What it is |
| family | string | yes |  | The page pattern this dataset fills (heddle families lists them). |
| only | list of strings |  |  | Publish only these row ids, where demand measures. |
| hub | string |  |  | Index page every row in this family links to, and the page that should link to every row. |
| ownPages | list of strings |  |  | Paths on the company's own site the families agent reads for every row, so what a row says about the company's own product comes from its own pages, cited like any other source. |
| fields | object |  |  | Fields this site's pages carry beyond the family's own (an answer under the H1, a meta description, FAQs), each with a note for the drafter. A row without one is skipped like a row without a required field, because its page would build broken. |
| rows | list of objects | yes |  | One row per page, with an id, its slug and the fields the family requires. A row missing a required field is left out. |
| rows[].sources | list of values |  |  | Where the row's facts come from: URLs, or { id, url, checked } that items cite by id. heddle validate fails a citation to a source not listed here. |
| rows[].visual | object |  |  | The row's own drawing (comparisons, alternatives). |
| pinned | list of objects |  |  | Pages kept outside this dataset that belong in its sibling lists, such as the headquarters metro's own page. Each carries a path, an anchor, and the row fields the family groups and clusters by. |
Generated from the Heddle repository at d2b6328.

## Questions

### How do I know my configuration is valid?

Run heddle validate with the site's path. It checks every file against its schema and the rules a schema cannot express, such as an agent above report with no approver, and exits 1 on any error.
