How-to: sitemap (live navigation editor)
Edit an existing model-driven app SiteMap's navigation tree in place — add or remove Areas, Groups, and SubAreas — without re-authoring the whole document. See the CLI reference for every flag.
These verbs complement app build-sitemap / app set-sitemap (which POST a whole
new SiteMapXml). They operate over a read-modify-write path: GET the live
sitemapxml, mutate the parsed XML tree, PATCH it back, then optionally publish.
Find the sitemap GUID
All sitemap verbs take a SITEMAP_ID positional argument — the sitemap record's
GUID. Retrieve it by name:
crm --json query odata sitemaps --select sitemapname,sitemapid
The sitemapid in data[] is what you pass as SITEMAP_ID.
Add an Area
crm --json sitemap add-area <SITEMAP_ID> \
--id cwx_sales --title "Sales" --solution cwx_crmworx --publish
--solution is required on every mutating sitemap verb — a component created
without an explicit target solution would otherwise land only in the system
Default Solution. Pass --solution Default for a deliberate
Default-Solution-only write.
--id must match [a-zA-Z0-9_]+ and be unique across all node ids in the
document (every Area / Group / SubArea Id — so remove-node --id is never
ambiguous). A publisher prefix (e.g. cwx_) is recommended. --icon accepts a
path string or
the $webresource:<name> directive. --show-groups sets ShowGroups='true' on the
new Area.
Add a Group under an Area
crm --json sitemap add-group <SITEMAP_ID> \
--area cwx_sales --id cwx_salesgrp --title "Customers" --solution cwx_crmworx --publish
--area is the Id of the parent Area (must already exist). The new Group Id must be
unique across all node ids in the document.
Add a SubArea under a Group
A SubArea requires exactly one content binding: --entity, --url, or
--dashboard. Passing more than one, or none, is a usage error.
# Bind a table by logical name (validated to exist in the org)
crm --json sitemap add-subarea <SITEMAP_ID> \
--area cwx_sales --group cwx_salesgrp \
--id cwx_accounts --entity account --title "Accounts" --solution cwx_crmworx --publish
# Link to a URL (including an HTML web resource)
crm --json sitemap add-subarea <SITEMAP_ID> \
--area cwx_sales --group cwx_salesgrp \
--id cwx_dashboard --url "/WebResources/cwx_dashboard.html" --solution cwx_crmworx --publish
# Link to a URL and append Dynamics context params (userid, orgname, orglcid, userlcid)
crm --json sitemap add-subarea <SITEMAP_ID> \
--area cwx_sales --group cwx_salesgrp \
--id cwx_ext --url "https://portal.example.com/widget" --pass-params --solution cwx_crmworx --publish
# Open a dashboard by GUID (sets DefaultDashboard)
crm --json sitemap add-subarea <SITEMAP_ID> \
--area cwx_sales --group cwx_salesgrp \
--id cwx_pipe --dashboard <dashboard-guid> --solution cwx_crmworx --publish
--entity is validated live — a logical name that does not exist in the org is
rejected before the PATCH, because a dangling Entity= silently hides the SubArea
in the UI.
--dashboard is validated live — the GUID must resolve to an existing
systemform with type == 0 (a dashboard). A well-formed GUID that doesn't exist
raises "no dashboard with id … exists"; a GUID of a non-dashboard systemform (e.g.
an entity form, type != 0) raises "not a dashboard". A dangling DefaultDashboard
renders a broken tile at runtime.
--pass-params is only valid with --url. It emits PassParams="true" on the
new <SubArea>, which tells Dynamics to append context parameters (userid,
orgname, orglcid, userlcid) to the navigated URL. Combining it with --entity
or --dashboard is a usage error (exit 2).
There is no SubArea WebResource attribute. A web-resource-backed SubArea uses
--url pointing at the web resource URL. The $webresource: prefix is the --icon
directive only.
The new SubArea Id must be unique across all node ids in the document.
Move (reorder) a node
Reorder an existing Area, Group, or SubArea within its parent without touching its attributes or children. Exactly one destination mode is required:
# Move directly before a sibling (same parent and node type)
crm --json sitemap move-node <SITEMAP_ID> --id cwx_accounts --before cwx_contacts --solution cwx_crmworx --publish
# Move directly after a sibling
crm --json sitemap move-node <SITEMAP_ID> --id cwx_accounts --after cwx_contacts --solution cwx_crmworx --publish
# Move to a 0-based position among same-type siblings
crm --json sitemap move-node <SITEMAP_ID> --id cwx_accounts --index 0 --solution cwx_crmworx --publish
The anchor for --before / --after must share the moved node's parent and node
type (e.g. you cannot anchor a Group move on an Area Id). --index must be in
range. A mismatch or out-of-range index is a clear error — no write is issued.
move-node is a pure permutation: it only repositions the node; its attributes,
children, and descendant structure are never modified.
Remove (or comment out) a node
# Hard delete — removes the node and its descendants
crm --json sitemap remove-node <SITEMAP_ID> --id cwx_accounts --solution cwx_crmworx --publish
# Soft delete — replaces the node with a well-formed XML comment
crm --json sitemap remove-node <SITEMAP_ID> --id cwx_accounts --comment-out --solution cwx_crmworx --publish
remove-node warns when the target is an Area or Group that has descendants
(cascade warning surfaced in meta.warnings). The command proceeds — pass
--dry-run first to preview exactly which subtree would be swept.
Set localized titles on a node
set-title writes <Title LCID="…" Title="…"/> elements inside a <Titles> block
on an Area, Group, or SubArea. Pass --lcid and --title as a pair; both flags are
repeatable and paired positionally (first --lcid matches first --title, and so
on).
# Set a single English title on a node
crm --json sitemap set-title <SITEMAP_ID> \
--id cwx_sales --lcid 1033 --title "Sales" --solution cwx_crmworx --publish
# Set titles for multiple languages in one call
crm --json sitemap set-title <SITEMAP_ID> \
--id cwx_sales \
--lcid 1033 --title "Sales" \
--lcid 1031 --title "Vertrieb" \
--solution cwx_crmworx \
--publish
--lcid must name an installed language. Before the PATCH, the CLI calls
RetrieveProvisionedLanguages and rejects any LCID that is not provisioned on the
org. A <Title> for an un-provisioned language is silently ignored by the platform,
so the rejection is intentional — install the language pack first.
One Title per LCID — updates in place. If a <Title> for that LCID already
exists, it is updated rather than duplicated. The XSD permits duplicate LCIDs, but
the CLI enforces uniqueness: passing the same --lcid twice in a single call is a
usage error (exit 2), as is a non-4-digit --lcid, a blank --title, a blank
--id, or a mismatched count of --lcid and --title flags — all validated
up front before any server call. (An LCID that is well-formed but not an
installed language is caught by the live check and reported through the normal
error envelope, exit 1.)
ResourceId is never touched. The platform-owned localized-label pointer is
left intact; only the inline Title= attribute of the <Title> element is written.
Strict child-element ordering is preserved. Within a node, the XSD requires
<Titles> before <Descriptions> before child nodes (Group/SubArea). When the CLI
splices in a new <Titles> container it inserts it at the correct position — never
after child nodes, which would be schema-invalid and fail on import.
The JSON response echoes action, node_id, and titles as a list of
{lcid, title} objects:
{ "ok": true,
"data": {"sitemapid": "…", "action": "set-title", "node_id": "cwx_sales",
"titles": [{"lcid": 1033, "title": "Sales"}, {"lcid": 1031, "title": "Vertrieb"}],
"updated": true, "published": true},
"meta": {} }
Set localized descriptions on a node
set-description writes <Description LCID="…" Description="…"/> elements inside a
<Descriptions> block. The shape is identical to set-title — --lcid and
--description are paired positionally and repeatable.
# Set a single English description
crm --json sitemap set-description <SITEMAP_ID> \
--id cwx_sales --lcid 1033 --description "Sales area" --solution cwx_crmworx --publish
# Set descriptions for multiple languages
crm --json sitemap set-description <SITEMAP_ID> \
--id cwx_sales \
--lcid 1033 --description "Sales area" \
--lcid 1031 --description "Vertriebsbereich" \
--solution cwx_crmworx \
--publish
All the same rules apply as for set-title: malformed input (duplicate or
non-4-digit --lcid, blank --description/--id, mismatched counts) is a usage
error (exit 2) validated before any server call; a well-formed but un-provisioned
--lcid is rejected by the live check (exit 1); ResourceId is untouched; and
strict <Titles> → <Descriptions> → child-node ordering is respected.
The JSON response echoes action, node_id, and descriptions as a list of
{lcid, description} objects:
{ "ok": true,
"data": {"sitemapid": "…", "action": "set-description", "node_id": "cwx_sales",
"descriptions": [{"lcid": 1033, "description": "Sales area"}],
"updated": true, "published": true},
"meta": {} }
Note on the legacy
Title=/Description=attributes. The SiteMap XSD deprecates the plainTitleandDescriptionattributes on nodes in favour of the<Titles>/<Descriptions>child elements.add-area,add-group, andadd-subareastill write the legacyTitleattribute — that is unchanged. Useset-title/set-descriptionwhen you need true per-LCID localisation.
Publish-gated read-back — and why not to chain staged edits
Gotcha —
sitemapxmlreads/writes go through the published layer. A Web API GET forsitemapxmlreturns the last published snapshot, not a staged edit (on on-prem v9.x especially). Everysitemapverb stages by default (noPublishAllXml), so a staged edit does not appear in a re-fetch until you publish.
This has a sharp consequence for multiple edits to the same sitemap: because
each verb reads sitemapxml fresh before mutating, a second staged (flagless)
edit re-reads the published layer (without the first edit) and PATCHes over
it — silently discarding the first unpublished edit. So do not chain
staged edits against one sitemap.
Instead, let each edit publish before the next reads — pass --publish on
every edit (it runs PublishAllXml and a T3 read-back inside the verb) so
sequential commands are safe:
crm --json sitemap add-area <SITEMAP_ID> --id cwx_ops --title "Operations" --solution cwx_crmworx --publish
crm --json sitemap add-group <SITEMAP_ID> --area cwx_ops --id cwx_opsgrp \
--title "Ops Group" --solution cwx_crmworx --publish
crm --json sitemap add-subarea <SITEMAP_ID> --area cwx_ops --group cwx_opsgrp \
--id cwx_contacts --entity contact --solution cwx_crmworx --publish
By default every edit stages (no --publish needed to write it — just to
see it in a re-fetch). Reserve a plain flagless (staged) edit for a single
change you publish yourself afterward (e.g. crm solution publish-all) — not
for batching several edits to the same sitemap.
Solution scoping
crm --json sitemap add-area <SITEMAP_ID> \
--id cwx_ops --title "Operations" \
--solution cwx_crmworx --publish
--solution is mandatory — there is no profile default and no opt-out; pass
--solution Default for a deliberate Default-Solution-only write.
Preview without writing
The global --dry-run flag reads the live sitemap for real (to validate parent
references, check uniqueness, and resolve entities) but issues no PATCH:
crm --json --dry-run sitemap add-area <SITEMAP_ID> --id cwx_ops --title "Operations" \
--solution cwx_crmworx
--solution is still required under --dry-run — it is validated before any
backend call.