Update site plan
update_site_planWrites the site plan (.xodia/plan.json) on a branch as an unsaved change. Send one of: plan with the whole plan or only the top-level fields you change (brief, inspiration, brand, audience, goals, motion, sitemap, pages, globals, library; the rest keeps its values; removing or renaming a page updates the nav and footer), pageStatus to set pages to planned, wireframed or built (one { path, status }, or a list), or globalsStatus for the shared nav and footer. Statuses are read from the site, so pageStatus and globalsStatus are optional: Xodia sets them when a request finishes and on sync_site_plan. With plan, always pass the etag from your last get_site_plan (null when there was no plan): if someone changed the plan since, it answers plan_conflict and you read again. pageStatus or globalsStatus alone needs no etag (they set one field on the plan as it is now); an etag you do pass is checked. Refuses a plan that isn't valid with each problem and where it is. Keep the user's brief and inspiration notes as they wrote them. Limits: 40 pages, 4 levels, 20 sections a page, 5 inspiration sites. Every field is optional and defaults to empty. Fields (lists in brackets are the allowed values): brief {name, business, audience, markets[], locales[], look, existingContent, links[] {url, label}}: the user's words, keep them. inspiration[] {url, likes[] (layout, colours, fonts, tone, motion), note, takeaways, reachable (true, false or null)}: the user writes url, likes and note; you write takeaways and reachable. brand {name (from brief.name, never invented), pitch, toneOfVoice, colours[] {name, value (#hex), role (suggested: primary, secondary, accent, background, surface, text, muted, border)}, fonts[] {family, role (heading, body, accent, mono), source, locale?}, logo (a repo path or null), iconSet? (the site's one icon set: lucide, tabler, heroicons, phosphor, iconoir, mingcute, material-symbols, radix, bootstrap; left out: keep the site's own set, else lucide)}. Fonts are one per role and locale: a font with a locale (one of audience.locales) serves that language beside the default one. audience {markets[], personas[] {name, description, needs}, goals[], locales[] (language codes, default first)}. goals {primaryAction {kind (book, buy, sign_up, contact, enquire, call, visit, subscribe, download, other), label, target (a page path or https URL)}, successMeasures[]}. motion {level (none, calm, lively, expressive; default calm), signature (the one memorable moment, in words), principles[] (up to 8 short rules), reducedMotion ("respect", always)}. options? {motionFeel? (soft, crisp, springy, cinematic; default soft), copyMode? (what Build writes: placeholder, draft, keep; default draft; wireframes always use placeholder copy), voice? {use[]?, avoid[]? (up to 20 words or phrases each), readingLevel? (simple, standard, expert; default standard)}, internalLinks? (default true), externalLinks? (none, cite, recommend; default cite), visuals? (what fills image slots: placeholder, svg, mixed; default mixed)}: the owner's build options. Every field may be left out (its default applies); keep the ones the owner set. sitemap[] {path, title, purpose, seo {title, description, keyword? (the main search phrase; leave it out on thank-you and legal pages), noindex? (true keeps the page out of search and the XML sitemap: thank-you and legal pages)}, children[]? (may be left out)}: "/" first. Site size counts content pages only; legal, thank-you and not-found pages come on top. pages {"": {sections[] {id (lowercase-dashes, unique on the page), type (from the section library, by group: opening: hero, page-header, announcement; story: about, story, mission, timeline; offer: features, benefits, services, menu, products, pricing, comparison; proof: testimonials, logos, stats, case-studies, reviews; people: team, careers; media: gallery, video, portfolio-list, portfolio-item; help: faq, process, contact, map, schedule, booking, form; content: blog-list, blog-post, events-list, event-item, content; close: cta, newsletter, footer-cta; any other short name is a custom type with no layouts), variant? (a layout of that type, listed by get_site_plan), motion? (reveal, stagger, parallax, marquee, pinned, counter, hover-lift, image-zoom, video-loop, page-transition, svg-draw, svg-morph, icon-motion, text-reveal, list-motion, beam, tilt-spotlight, scroll-story, ambient, none), visuals? (this section's image slots over options.visuals: placeholder, svg, mixed), list? {collection, filter?, limit?} (the section shows a collection's items), purpose, contentNotes} (each section's id is the data-section id of its element on the site), status (planned, wireframed, built; read from the site, keep what get_site_plan shows), inNav? (a hint used only when the menu is generated or inherited: absent means top-level pages in; legal, thank-you, nested and dynamic pages out; globals.nav.items decides the menu), navLabel? (a short menu label), prompt? (the owner's note for this page, keep it), template? (the page template it started from; template kinds: home, about, services, menu, pricing, contact, booking, events, blog-index, blog-post, portfolio-list, portfolio-item, faq, careers, legal, thank-you), collection? (a collections[] id: this is that collection's item page, path //:slug), localized? {"": {title, seo {title, description, keyword?}}}}}: localized holds other languages' title and SEO; each locale must be in audience.locales. globals {nav {items[] {path, label? (left out: the page's navLabel, else its title), children[]? (page paths, one level; may be left out)}, cta? {label, path or url} (the one menu button), layout (simple, centered, split, minimal), mobile (drawer, full-screen, inline), sticky, announcement? {text, path or url}}, footer {columns[] {title, links[] {label, path or url}}, contact, social, newsletter, legal[] (page paths as strings, such as "/privacy"; request texts show them resolved as {path, label}, but you write paths), layout (columns, simple, big-cta)}}: the shared nav and footer. nav.items is the menu, exactly as written: never "/" (the logo links home) and never the cta's page again as an item. Links point at sitemap pages (not dynamic ones) or https addresses; up to 12 menu items. Removing a page drops its links and renaming one keeps its place. Sending globals makes nav.items the menu as you wrote it; leaving globals out keeps them and lets new top-level pages join. Their status is read from the site (globalsStatus is optional), never set here. library {sections[] {id, name, type, variant?, purpose, contentNotes, motion?, visuals?}, pages[] {id, name, purpose, sections[]}}: sections and pages the owner saved to reuse. Keep it unless asked. collections[]? {id (lowercase-dashes, the content/ folder), name, purpose, starter? (blog, projects, team, products, events, jobs, testimonials, faq), fields[] {id (camelCase), type (CMS field types: plainText, richText, image, multiImage, videoLink, link, email, phone, number, date, dateTime, switch, color, option, file, reference, multiReference), label?, collection? (reference targets)}, urlFolder? (item pages on //:slug)}: CMS collections the site needs (blog, projects, team, menu, events...), up to 20. Each gets a collection page: a sitemap page on //:slug whose pages entry has collection: "". A section that lists items has list {collection, filter? (in words, like "featured, newest first"), limit?}. Sync copies whether a collection exists, its fields and URL folder from the site (onSite marks it); purpose stays yours. Icon sets (brand.iconSet takes a ui set; lucide, tabler, heroicons, phosphor, iconoir, mingcute, material-symbols, radix, bootstrap, line-md, simple-icons in all): get_site_plan lists each with its packages and when to use it.
Tool call template
Replace each placeholder with a value matching the input schema. Only required parameters are included; add optional parameters as needed.
{
"id": 1,
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"arguments": {
"projectId": "<projectId>"
},
"name": "update_site_plan"
}
}Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| etag | No | The etag from your last get_site_plan; null when it said there is no plan file yet. Required with plan. Optional with pageStatus or globalsStatus alone (checked when you give it). A mismatch answers plan_conflict: read again and reapply your change. | |
| plan | No | The plan (file version 2), or only the top-level fields you change: brief, inspiration, brand, audience, goals, motion, options, sitemap, pages, globals, library, collections. Fields you leave out keep their current values. version, updatedAt and updatedBy are set for you. Field shapes and allowed values are in the tool description. | |
| branch | No | Default: the branch the workspace has open. Pass the request's branch when a request names one. | |
| projectId | Yes | Project id from list_projects | |
| pageStatus | No | Instead of plan: one page's status { path, status }, or a list of them. Optional: Xodia reads page statuses from the site (sync_site_plan, and after each request), so you don't need to set them. | |
| globalsStatus | No | Instead of plan and pageStatus: the shared nav and footer's status, "wireframed" or "built" ("planned" resets it). Optional: Xodia reads it from the site when a globals request finishes. |