commcare nova

Tools

What your agent can do once it's connected to Commcare Nova.

These are the tools the Commcare Nova MCP server exposes. You don't call them directly, your agent does, in response to natural-language instructions. Knowing what's available helps you write better prompts and understand what your agent is doing.

Easy starting point

get_agent_prompt returns the same instructions Commcare Nova's own web builder follows when it builds apps. Tell your agent to call it on its first turn (with mode: "build" for a new app or mode: "edit" for an existing one) and follow the text it returns. You don't have to do this, your agent can drive the tools however it wants, but starting from the instructions Commcare Nova already tunes for these tools is the fastest path to good results.

It's a long result, a whole system prompt. A prompt that fits the model-facing result budget arrives as plain text. A larger prompt instead arrives as a nova-agent-prompt-page JSON object. Have your agent save its prompt_chunk, call the tool again with the same mode and app_id plus next_cursor, and repeat until complete is true. offset_unit is unicode-code-points: chunk_start, chunk_end, and prompt_length count Unicode code points, not UTF-16 code units or UTF-8 bytes, and a chunk never splits a surrogate pair. Each page must have the same prompt_sha256, each chunk_start must equal the preceding chunk_end, and the final chunk_end must equal prompt_length. The agent can then concatenate the chunks in order and follow the assembled prompt. If the app changes while those pages are being fetched, CommCare Nova refuses to mix the snapshots and asks the agent to restart without a cursor.

The complete assembled prompt always ends with the line NOVA-PROMPT-END. Have your agent check for it and stop if it's missing. Pagination keeps all guidance and the complete app summary; it does not shorten either one to fit.

Stable identities and structured content

Read an app before editing it. Commcare Nova returns stable UUIDs for modules, forms, fields, choices, case-list columns, search inputs, automations and their nested criteria, updates, recipients, events, and filters, worker-information properties, roles, personas, organization levels, place-information properties, places, case operations, after-submit links, Project data tables, columns, and rows. Editing tools address those objects by their UUID parameters, such as moduleUuid, formUuid, fieldUuid, columnUuid (the case-list column), searchInputUuid, operationUuid, and linkUuid, rather than by a display name, saved field id, path, list position, or operation id. Project data tables and columns carry their UUID on id. Names, saved ids, lookup tags, and column export names remain readable, editable authoring values; changing one does not retarget a UUID-linked reference.

When one item in a creation call will be referenced by another item in that same call, give it its final UUID in the matching creation slot: moduleUuid, formUuid, fieldUuid, optionUuid, columnUuid, searchInputUuid, or operationUuid. Otherwise, omit that slot and let Commcare Nova mint the UUID. A same-call group or repeat parent must appear earlier in the field list and declare fieldUuid. An operation whose result is consumed by a later operation must likewise appear earlier; predeclaring identity does not change execution order.

Every successful creation result returns the final identities it created in input order, including identities Commcare Nova minted. Field receipts nest their inline choice UUIDs in source order; create_form nests all field and choice receipts under its form, and create_module nests forms, fields, choices, and born case-list columns under its module. Continue from that result: no read-after-write is required just to discover a new UUID.

Machine-authored inline choices have exactly { "optionUuid"?: "...", "value": "...", "label": { "parts": [...] } }. A choice's value is the answer the app stores, not its wording: a lowercase slug with words joined by underscores (prefer_not_to_say), unique within the field and kept stable once data exists. It can't hold spaces, quotes, or apostrophes, because the device refuses a choice value with a space in it and a multi-select answer is a space-separated list of these values. The wording people read goes in label. A value that breaks that shape is refused at the tool's input schema, and the refusal restates the shape; an app that already holds such a value fails validation with a finding that names the replacement slug for that exact choice. Stored uuid, option media, and identity aliases are rejected on every field writer. Lookup-backed choices use the canonical tableId / valueColumnId / labelColumnId fields, plus optional filter; there are no tableUuid or column-UUID aliases. Option media is changed through the dedicated media tools.

MCP expression slots take Commcare Nova's typed AST, not an XPath source string. For example, a field reference is a part such as { "kind": "field-ref", "uuid": "<field UUID>" }; a complete XPath value is { "parts": [/* text and typed reference parts */] }. Reference-capable prose uses the same outer { "parts": [...] } shape with explicit reference atoms. Plain text that happens to contain #form/name stays plain text. Predicate and ValueExpression leaves likewise use UUIDs, for example { "kind": "field", "uuid": "<field UUID>" }, { "kind": "input", "searchInputUuid": "<search input UUID>" }, { "kind": "id-of", "opUuid": "<operation UUID>" }, and { "kind": "session-user-property", "userPropertyUuid": "<property UUID>" }.

There are exactly two name-keyed leaves, both deliberate: a case property, which uses (caseType, property), and a CommCare session field Commcare Nova does not own, which names one field out of a closed framework set.

This is only the machine contract. In Commcare Nova's visual XPath editor, people still type and read friendly expressions such as #form/first_name; Commcare Nova resolves that text to identity when the edit commits and prints the current friendly path when it is opened again. Users are never asked to write a UUID-shaped XPath.

Lookup expressions use the same identity rule. A row-scoped column is { "kind": "table-column", "tableId": "...", "columnId": "..." }; table lookups and lookup-backed choice sources likewise use tableId and column ids, never table tags, export names, or display labels.

Browsing apps

ToolWhat it does
list_appsList your apps with optional status filter, sort, and pagination. Each entry names the Nova Project the app lives in (project_id + project_name).
search_appsFuzzy search by name. Case-insensitive, tolerates typos. Entries carry the same Project columns as list_apps.
get_appSummarize one app: modules, forms, fields, case types, worker information, roles, personas, and its organization model. The summary opens with the app's Nova Project, so your agent always knows whose workspace it's editing.
check_project_space_compatibilityCheck whether one explicitly selected CommCare HQ project space can run the app. Returns required app capabilities, blockers, non-blocking performance guidance, and next steps without changing the app or project space.

Languages and translations

App languages use the same source/default/target overlay as the visual Builder. The language catalog is not an AI allowlist: an agent may add, copy, read, and manually translate any individual living language whether or not Nova offers automatic translation for that exact direction. A language is an identity object {language, script?, region?}, never a combined code string: language is an ISO 639:2023 Set 3 code for one individual living language (cmn, spa, hin); script is an ISO 15924 code (Hans), required exactly when the language has more than one customary writing system; region is an ISO 3166-1 alpha-2 code (MX), always optional. Macrolanguages (zho), two-letter codes (zh, es), and non-living codes are rejected with the identifiers to use instead; a macrolanguage rejection lists its individual members by name. The worker-facing name and text direction derive from the identity itself rather than being authored.

get_languages reports automatic translation as Available, Not evaluated, or Withheld for each source-to-target pair. Available means both languages are distinct members of Nova's checked-in 57-language launch set; the language axis alone decides it, so two writing systems of one language are never a translatable pair. It is not a claim of provider-published coverage or per-direction bilingual certification. The bounded write tools below remain the complete MCP surface: MCP can inspect availability and author or copy translations, but it has no paid automatic-translation action.

ToolWhat it does
get_languagesRead the ordered language catalog, source and default languages, per-target coverage, and diagnostics for content the static app-language catalog cannot translate.
get_translatable_contentPage through the snapshot-bound translation inventory for one language, including source/effective values, status, breadcrumb context, and protected reference parts.
add_languageAdd one target and atomically seed every current unit from an existing copyFrom language.
update_languageSet the runtime default, or replace the sole source language's identity.
remove_languageRemove a target language and its overlay. The source language and the final remaining language cannot be removed.
update_translationsAtomically set, clear, or review at most 50 distinct target units using stable unit IDs and exact review fences.

Read get_languages, then call get_translatable_content for the target before writing. A set operation must repeat the unit's current sourceFingerprint as expectedSourceFingerprint; it is machine-authored and starts as Needs review. A review operation repeats the explicit entry's stored fingerprint as expectedSourceFingerprint, the unit's current fingerprint as expectedCurrentSourceFingerprint, and the exact target value returned by the read. These independent fences refuse the whole batch if either side changed concurrently. Prose values keep typed reference parts; do not flatten a protected field reference into its displayed text.

add_language always copies a complete effective projection. If the copy source itself falls back for one unit, the new target receives the value workers would actually see. Missing and out-of-date target entries also fall back to the current canonical source at runtime, so incomplete translation work never creates an invalid app.

Checking project-space compatibility

check_project_space_compatibility is a read-only destination check. It needs both app_id and an explicit domain, and it checks exactly that CommCare HQ project space without compiling, uploading, or changing either side.

Call get_hq_connection first to list the available project spaces. Use the domain the user selects, and never choose among several project spaces on their behalf. The check requires nova.hq.read because it contacts CommCare HQ.

The result returns project_space_compatibility with:

  • status: not_needed, ready, or blocked for the checked destination
  • target_domain: the exact project space that was checked
  • required_capabilities: the app features that need project-space support, including why the app uses each one and whether it is available, missing, or unverified
  • blockers: required capabilities that are missing or could not be confirmed
  • advisories: guidance that never blocks publishing, including slower large Search results when the faster path is unavailable or unverified
  • message, support_email, and docs_url: a plain-language result and the next place to go

A missing or unverified required capability blocks a later direct publish before lookup tables, places, or the app are sent. An advisory never blocks. upload_app_to_hq performs its own authoritative check immediately before any remote write, even when the agent called check_project_space_compatibility earlier. A successful upload and a project_space_incompatible error both return project_space_compatibility so the agent can explain the checked result.

compile_app stays available because a download has no selected destination. When the app has destination requirements, its leading nova_project_space_compatibility result block contains project_space_compatibility with status: "not_checked". The same report is also available in _meta["nova/projectSpaceCompatibility"]. Check the actual destination later with check_project_space_compatibility.

Creating and deleting apps

ToolWhat it does
create_appCreate a publish-ready survey starter and return its app ID, sequence-1 blueprint, and starter module/form/field UUIDs. Continue with those identities; every later change is checked as it lands. Pass project_id (from list_projects) to create the app in a shared Project; omit it and the app lands in your personal Project.
delete_appSoft-delete an app. Recoverable within the returned window.

Projects and sharing

Every app lives in exactly one Nova Project, and the Project is the sharing unit: each of its members sees the Project's apps plus their case data and media. Your account starts with a personal Project, which is private by construction and can't be shared. When other people need to use what your agent builds, the flow is: create a shared Project once, create apps into it, and invite the humans.

ToolWhat it does
list_projectsList the Projects you belong to, with your role in each and which one is your personal Project. The returned project_id values are what every other Project-aware tool takes.
create_projectCreate a shared Project owned by you. Create one per program or team and reuse it across builds — Project deletion is disabled, so every Project you create is permanent.
invite_memberInvite a Dimagi email address to a shared Project as viewer, editor, or admin. Needs an admin or owner role there. No invitation email is sent: the invitee sees the invitation inside Commcare Nova the next time they sign in and accepts it there, so tell them to expect it. Invitations expire after 48 hours.
list_membersList a Project's members and pending invitations. Any member can call it. The returned member_id is the handle update_member_role takes.
update_member_roleChange a member's role to viewer, editor, or admin. Needs an admin or owner role there. The Project owner's role can't be changed.
move_appMove an app into another Project, bringing its case data, media, and chat history along in one transaction. Needs an admin or owner role in both the source and destination Projects, and the move can't take the app away from the source Project's owner: either the owner moves it themselves, or every source owner must already be a member of the destination. The response's result field says what happened — moved, or already_in_project when the app was already there (nothing moves; its case-data tenancy is verified and repaired where needed).

The roles: a viewer reads apps and data, an editor also edits them, an admin also manages members, and the owner is the member who created the Project. Owner isn't assignable — the creator stays the one owner.

Prefer creating an app in the right Project from the start. An app that references Project data tables or has captured form submissions can't move between Projects, so move_app is the recovery path for apps born in the wrong place (typically the personal Project), not the everyday flow.

A Nova Project is not a CommCare HQ project space. The Nova Project decides which people share the app inside Commcare Nova while it's being built; an HQ project space (domain) is where a finished app is published for workers. upload_app_to_hq targets an HQ domain and is unrelated to which Nova Project the app lives in.

Building a new app from a description

A new app is named Untitled and already contains one survey module, survey form, and text question. Design the requested app first, then refine that starter or replace it only after its valid replacement exists. update_app gives it the requested name. When the design uses custom worker information, add_user_properties declares it next, before generate_schema, modules, forms, conditions, or calculations can refer to it. generate_schema then commits the data model, and each create_module call lands one complete module, every call checked as it lands. Commcare Nova supports one submenu tier: create a top-level parent first, then pass its UUID as the child's parentModuleUuid. After a Connect build's forms exist, configure_connect sets the mode and complete UUID-addressed participant set in one call. Roles and personas can follow the app structure; the referenced worker information cannot.

ToolWhat it does
update_appSet the app's name.
configure_connectSet the exact app-wide CommCare Connect target atomically, or turn Connect off and clear every form block.
generate_schemaRecord the data model on the app: every case type with its properties and parent links. A real write, create_module references the recorded types by name, and a form field that writes a recorded property may inherit its intrinsic type, canonical label, and choice catalog. Field hint, requiredness, and validation remain specific to each form. Also how a new case type enters an existing app.

Configuring CommCare Connect

configure_connect owns enabling Connect, switching between learn and deliver, replacing the participant set, and disabling Connect. For an enabled mode, pass the complete nonempty set of participating forms. Every form is addressed by formUuid; an unlisted form is auxiliary and any old Connect block on it is cleared.

{
  "app_id": "app-id",
  "mode": "learn",
  "participants": [
    {
      "formUuid": "01890f45-0000-7000-8000-000000000002",
      "connect": {
        "learn_module": {
          "name": "Health basics",
          "description": "Introduction and guided practice",
          "time_estimate": 1
        }
      }
    }
  ]
}

time_estimate is the estimated whole hours needed to complete the module. Round up and use at least 1.

The Connect sub-block id is an emitted Connect identifier, not the address of the Commcare Nova form. Usually omit it: Commcare Nova derives it once and stores the final value. An explicit id must already be valid and unique, or the whole call is refused; Commcare Nova never silently rewrites it. The form itself is always selected by UUID.

To disable Connect and clear every form's Connect block in the same change:

{
  "app_id": "app-id",
  "mode": null
}

Do not include participants with mode: null. Duplicate or foreign form UUIDs, empty participation, incomplete blocks, mixed mode families, and duplicate Connect ids are refused before anything saves. Once a mode exists, update_form can refine one participant's block, but it cannot create the mode, cross mode families, or change which forms participate. Creation tools have no Connect slot; a newly created form is auxiliary until a later configure_connect call replaces the complete set.

Working with workers and personas

Worker information defines the values a worker can carry. Roles provide reusable defaults for those values, and personas are named Preview workers that can inherit a role and override individual values. Read first to get stable UUIDs: role and persona values name a worker-information property by userPropertyUuid, so changing its saved name never retargets the value.

Custom worker-information references use that same identity. In Predicate and ValueExpression inputs, use { "kind": "session-user-property", "userPropertyUuid": "..." }. The name-based { "kind": "session-user", "field": "..." } arm is for CommCare-provided or external worker fields that Commcare Nova does not own. Textual XPath in the visual editor shows the current #user/<saved-name> projection. In an MCP XPathExpression, a custom property is instead an explicit { "kind": "user-property-ref", "userPropertyUuid": "..." } part; an external property is a user-ref part. MCP never submits the projected name for Commcare Nova to resolve.

On update tools, leaving a field out keeps it and passing null clears a clearable field. Every batch is checked as one change; if any entry would make the app invalid, none of it saves. Role/persona updates use valuePatch for one UUID-addressed value at a time: a string sets it, null clears it, and omitting valuePatch leaves every value unchanged.

ToolWhat it does
get_usersRead worker information, roles, and personas, including their stable UUIDs and UUID-linked values.
add_user_propertiesAdd one or more worker-information properties. Returns their stable UUIDs.
update_user_propertyChange one property's label, saved name, required flag, or accepted values.
remove_user_propertyRemove one property and atomically clear its values from every role and persona. Refuses while any saved condition or calculation references it, naming the settings to update first.
add_user_typesAdd one or more reusable roles and their default worker-information values.
update_user_typeChange one role's name or description, or set/clear one UUID-addressed default with valuePatch.
remove_user_typeRemove one role. Refuses while a persona still uses it.
add_personasAdd one or more named Preview workers, optionally with a role and value overrides.
update_personaChange one persona's name, description, or role, or set/clear one UUID-addressed override with valuePatch.
remove_personaRemove one persona while preserving the cases that persona already owns.

Working with organizations and places

An organization has blueprint-owned levels and place-information properties, plus app-scoped place rows. Read it with get_organization before writing. Its opaque, snapshot-bound cursor pages one bounded stream across levels, properties, and matching places; accumulate each collection until page.complete. If a later page reports that the snapshot changed, restart without a cursor. Use query to narrow a large tree and request includeValues only when custom place values matter.

Every level, property, and place has a stable UUID. Names are editable labels; level codes and place site codes are create-once external identities. Place writes also use an exact organization revision: pass the revision from the complete read into the first write, then chain each successful result's revision into the next write. Re-read after a conflict. update_location supports a one-property valuePatch; a string sets the UUID-addressed value, null clears it, and values replaces the complete value bag.

When a saved reverse-hop owner rule requires a destination below every new source place, create the source with its complete descendants tree in one create_location call. The tree is structurally nested and bounded; nesting is parentage, so it introduces no request-local identity vocabulary. The compact result mirrors that tree with each server-minted final place UUID and site code. Sequential creates are refused because they would leave the source temporarily invalid.

A persona's locationUuids replace its complete place assignment in main-first order. Case flow controls ownership and delivery independently from address-book visibility. Reverse-hop place owners export on every mode: they travel as level codes and the case's own owner. A fixed place owner is Preview only, because it travels as Nova's own place id and would match nobody on a CommCare HQ project.

Archiving is a two-call confirmation flow. First call set_location_archived with archived: true and no confirmation, review its bounded impact and exact confirmation token, then repeat with confirm: true, expectedRevision set to the returned expectedRevisionForConfirmation (the impact revision), and the unchanged confirmedImpact. Never confirm a blocked preflight. The subtree is archived and persona assignments there are removed, but owned cases are never reassigned.

ToolWhat it does
get_organizationPage through the snapshot-bound organization model and matching place rows.
add_organization_levelsAdd levels parent-first and return their stable UUIDs.
update_organization_levelChange one level's editable settings by UUID.
remove_organization_levelRemove an unused level by UUID.
add_location_propertiesAdd UUID-addressed place-information declarations.
update_location_propertyChange one property's declaration, including its complete level applicability.
remove_location_propertyRemove one property and its stored values after reference checks.
create_locationCreate one place and, when required, one bounded structurally nested descendant tree atomically.
update_locationChange one place's editable data, retype it, or set/clear one UUID-keyed custom value.
move_locationMove one place within the valid level topology while preserving its UUID.
set_location_archivedPreflight and confirm subtree archive, or unarchive with the current revision.

Working with automations

Automations are canonical app objects, not commands that execute in Preview. Read them first and preserve the stable UUID on the automation and every nested criterion, setup-only instruction, case update, recipient, schedule event, and user-data filter that still represents the same item. A complete update removes any nested UUID it omits, but the server projects that desired state into the same granular mutation grammar used by the Builder and Solutions Architect.

commcare nova can describe automatic case-update rules and conditional alerts that CommCare HQ can represent. It never claims to install them: HQ exposes HTML setup pages and an alert-content spreadsheet import, but no REST resource for rules, alerts, or schedules. Reads and successful add/update results return freshly derived manual setup guidance plus the criteria omitted from local matching. Remove returns its deletion receipt only and leaves a rule already configured in HQ unchanged.

The returned guide identifies the exact current HTML route templates: /a/<domain>/data/edit/automatic_updates/ for automatic case updates and /a/<domain>/messaging/conditional/ for conditional alerts. CommCare HQ can be configured to run every active case-update rule for a case type whenever a case is saved. That behavior is project-wide, so it is a deployment caveat, not a field in commcare nova's per-rule schema.

The loaded schema mirrors the two current HTML forms without a shared criteria superset. Automatic updates accept value/date comparisons against case, parent, or host properties, at most one standard closed-parent condition, and the separate server-modified-age field; they do not accept regex conditions. Alerts accept value comparisons plus portable regex against direct case properties, and do not accept date, parent/host, closed-parent, or server-modified conditions. The closed parent is HQ's normal parent link, not a caller-selected index or extension. Names must be nonblank and already trimmed. Equality and fixed-update literals must be exact, nonblank, unquoted stored values, and alert regex patterns must be nonempty. Web users are not a recipient arm. Date conditions compare the current date directly with the property date plus a signed day offset. If the property is a datetime, HQ and Preview use its written calendar date and discard the time and explicit offset before adding days. Both automation families accept at most one UUID-backed location condition with an explicit descendant flag. HQ's runtime and form POST accept it, though the current visible editors hide the picker; returned guidance states the HQ-administrator application requirement. The condition remains locally countable from the organization snapshot and is never reduced to setup prose. Automation inputs always use commcare nova's standard property names. Returned setup guidance projects case_type to HQ type, case_name to name, date_opened to opened_on, and last_modified to modified_on; case_id, owner_id, and external_id remain unchanged. case_id and case_type are read-only. status is refused because the app stores open/closed text while HQ exposes a boolean field. Standard datetime properties allow date or blankness conditions, not equality or regex, and restart/event-time slots accept custom case properties only. After trimming, a case-property event-time value must begin with H:MM or HH:MM, and the whole value must parse as a time. Suffixes such as AM/PM or seconds are accepted; blank, nonmatching, or unparseable values use 12:00 PM. Concrete HQ worker/group recipient IDs must be nonblank and have no surrounding whitespace. Connect content also refuses matched-case, parent-case, all-child-cases, case-property-email, and case-group recipients. Returned guidance states that SMS Survey requires Inbound SMS access. Connect content requires CommCare Connect support in the project space, which Nova checks before publishing, and every resolved recipient must be a CommCare mobile worker with an active PersonalID link. Checkbox-style, case-property, and custom recipient kinds are singletons; list recipients cannot repeat a concrete target. Descendant controls require a location recipient, location-level filters require descendants, and each worker-property filter key appears once. Each filter value is either an exact literal (empty and surrounding whitespace are meaningful) or a structural case-property value with (caseType, property) identity. Never encode a lookup as brace-wrapped literal text: HQ interprets that string dynamically. Every triggering case must contain a referenced property because HQ raises when its direct lookup is missing. HQ applies filters only to user-account recipients, so the schema refuses filters with case, parent/child-case, case-email, case-group, or registered custom recipients; those results bypass filters or have an unknown runtime type. Multiple keys/values or exact empty/whitespace values require HQ's JSON filter mode and the returned guide names its system-administrator prerequisite.

Every message field is an AutomationMessageTemplate, not a magic-token string. Its parts are literal { kind: "text", text } values, explicit { kind: "case-property", scope, caseType, property } references, or closed { kind: "context-property", context, property } case-owner/recipient references. Token-looking text such as {case.case_name} stays literal; the guide doubles its braces before HQ's Python Formatter evaluates the result. Use a structural part only when HQ should substitute a value. Reads return the same canonical parts, while the generated guide alone projects them to HQ {case...} / {recipient...} syntax. Registered custom recipient/content IDs and setup-only condition text must be concrete, trimmed, and nonblank. A setup-only condition has an explicit ucr-filter or registered-custom kind; the returned guide names the former's project-space support and the latter's system-administrator save requirement. The returned guide also names HQ's system-administrator save requirement whenever a custom recipient or custom content handler is present; registration plus project-administrator access is not sufficient.

Custom case properties named owner, host, or last_modified_by cannot be used in a message case-property part in case, parent, or host scope. HQ adds those names to its formatter context before it adds custom case data, so the framework values shadow same-named custom properties. Rename the custom property, or use a context-property part when the intended value is the real case-owner or recipient context.

Conditional-alert schedules are valid only when one HQ setup form can express them. Every event uses one schedule-wide content type, and immediate events after the first wait at least five minutes. Custom Daily events use one timing mode and remain ordered; Weekly and Monthly schedules also share timing and content, with Monthly limited to days 1–28 and -3–-1. Weekly and Monthly days are unique closed choices in HQ's canonical order. A Weekly day is stored as an offset from startDayOfWeek; changing the start must remap and re-sort offsets to preserve the selected absolute weekdays. For a fixed date, setup guidance omits HQ's hidden Custom Daily start-offset control and explains that HQ derives the hidden Weekly start weekday from the date. Random windows cannot overlap. Survey reminder intervals must total less than the expiration window, and case updates in partial submissions require partial submission itself. A timed restart property is valid only with a rule-trigger start. A single zero-delay immediate event maps to Immediately; any delayed or repeated immediate schedule maps to Custom Immediate Schedule. These are input-schema constraints shared by SA and MCP, not post-save warnings.

Email content has one discriminated body: plain-text carries message and targets a project without the domain-level Rich text emails toggle; rich-text carries only html and requires that toggle. HQ sanitizes and rewraps rich HTML and derives its plaintext, so do not send parallel bodies or promise byte-exact rich output. Every email event in a schedule uses the same body kind.

Builder Preview may count current real case rows against each kind's ordinary property criteria, including automatic-update parent/host reads, and the automatic-update closed-parent condition. It never updates a case, sends a message, or advances a schedule. UCR filters, instance-registered custom criteria, and HQ server-modified age have no honest local evaluator; carry the first two as distinct setup-only kinds. The Builder names those omissions beside its partial count; MCP automation results return the same omission list without pretending to return or execute a Preview count. For locally countable property criteria, whitespace-only strings are blank and equality matches only an exact stored string. Stored non-string values never equal the configured string and therefore satisfy inequality. A parent/host equality or inequality requires that related case to exist. Parent reads use the parent identifier regardless of relationship. Host reads are admitted only while the app model leaves one unambiguous canonical extension relation for the automated case type. If an advanced case operation can add a second extension relation, commcare nova refuses every host-scoped criterion, update target, update source, and message case-property part instead of choosing from HQ's unordered extensions; the extra link and parent-scoped references remain valid. Every host-scoped reference also requires exactly one live extension at runtime. Retained extra extension indices make the current-match count unavailable, and HQ does not define which extension it chooses as the host. Alert regex matching applies only to stored strings; commcare nova does not coerce numeric or boolean case values to text. Date conditions use the same relation selection and compare only the property's written calendar date, never its time of day.

ToolWhat it does
get_automationsRead every automation in display order, including stable nested UUIDs, regenerated setup guidance, and an explicit statement that Preview does not execute it.
add_automationsAdd one or more complete automatic case-update rules or conditional alerts atomically and return each regenerated guide and omission list. Caller-supplied UUIDs are final identities and must be globally unused.
update_automationSet one automation's complete desired state and return its regenerated guide and omission list while preserving its immutable UUID and kind. Existing nested UUIDs preserve identity; omitted nested items are removed.
remove_automationRemove one automation and its derived guide from commcare nova without claiming to change CommCare HQ.

Working on modules

Modules may be top-level or may sit one level inside a top-level parent. This menu relationship controls navigation only. It is separate from a case type's parent_type data relationship, so neither implies the other. A child cannot contain another child, and a parent with children stays top-level until those children move or are removed.

Every form has one canonical module. Commcare Nova does not author linked or shadow copies of one form under several menus. When you need several views of the same cases, use case-list filters and design the menu structure deliberately.

ToolWhat it does
create_moduleAdd a new module together with its forms (each with fields) and its case-list columns, in one call. Omit parentModuleUuid for top-level, or pass an existing top-level parent's UUID for a child. Its case type must already be recorded on the app (generate_schema). When the new module is born with a follow-up or close form that applies once to several chosen cases, selection: { kind: "multiple", maximum: N } creates that workflow atomically. A module lands complete or not at all. It returns every created UUID; predeclare only identities referenced inside the call. Refine the case list afterward via the case-list tools below.
update_moduleRename a module and/or set its case type, addressed by moduleUuid (with starter case-list columns when the module has none; the result returns those born column UUIDs), set the case type before adding registration, follow-up, or close forms to a module created without one.
remove_moduleDelete a module by moduleUuid. A parent must have no child modules before it can be removed.
move_moduleReorder or reparent a module without changing its identity. after is always required and names a sibling in the destination menu, or is null for first. Omit parentModuleUuid to reorder within the current menu, pass null to make the module top-level, or pass a top-level module UUID to place it inside that menu.
get_moduleRead one module by moduleUuid, including its parent_module_uuid, ordered child_module_uuids, form and field UUIDs, structured case-list config, display condition, and current icon and audio label.

Working on the case list

A module's case list is the screen that lists existing cases and the search bar that filters them. Its configuration covers the columns shown in Results and Details, an optional filter predicate, the search inputs the user types into, their ordering, an optional card layout, and whether a form opens with one case or an ordered set.

ToolWhat it does
configure_case_listConfigure a known case-list refinement in one call: add columns and search inputs, set or clear the filter, compose the search-screen display, and arrange Results, Details, and search-input order. Omitted parts stay unchanged; the result returns created UUIDs.
add_case_list_columnsAdd one or more columns to the case list (plain / date / phone / id-mapping / image-map / interval / link / calculated). Each may predeclare columnUuid; the result returns the final UUIDs.
update_case_list_columnReplace a column's body, addressed by moduleUuid and columnUuid.
remove_case_list_columnRemove one column by moduleUuid and columnUuid.
reorder_case_list_columnsReorder a surface by passing its complete columnUuids sequence.
set_case_list_filterSet or clear the optional filter predicate that narrows the list.
add_search_inputsAdd one or more search inputs (simple property bind or advanced predicate) in one call. Each may predeclare searchInputUuid; predicates reference inputs by that UUID.
update_search_inputReplace a search input's body, addressed by moduleUuid and searchInputUuid.
remove_search_inputRemove one search input by moduleUuid and searchInputUuid.
reorder_search_inputsReorder search inputs by passing their complete searchInputUuids sequence.
configure_case_selectionPass selection: { kind: "multiple", maximum: N } to let workers choose several cases, where N is 1 through 100. Pass selection: null to return to opening one case at a time. A several-case module needs a follow-up or close form that can consume the batch. Questions saved to the selected case start without one case's value; each nonblank answer is saved to every selected case, while blank preserves their existing values. A capture destination updates only when the runtime submission provides a file; according to its configured mode, each case stores either that attachment or its published link. Preview creates neither. applied and unchanged are complete outcomes. needs_changes applies no mutations. For needs: "confirmation", review the returned effects, then repeat the same request with confirmedModuleUuids exactly equal to requiredConfirmedModuleUuids and confirmationToken copied unchanged from that same result. The token binds the approval to the requested source selection and every reviewed module effect. If that effect changed, no mutation is applied and the result supplies the current review and a new token. For needs: "repair", fix the UUID-owned blockers before retrying. For needs: "refresh", call again without either confirmation field to review the current effects. unavailable also applies nothing and returns the reason the request cannot run against the current app.
set_case_list_tileLay the case list out as a tile — a 12 x 12 grid where each field shown in Results occupies a rectangle — and place its fields on that grid. tile turns the layout on or off (null turns it off and keeps every placement), placements moves fields, and both may ride one call. While the tile is on, every field shown in Results needs a place and no two fields may share a square, so the whole layout lands in one call.

set_case_list_tile also carries grouping, through tile.grouping. It shows the cases that share a connection together, under one heading:

FieldWhat it does
identifierThe name of the case connection to group by, almost always parent. Grouping is by a connection, never by a property value: the heading is drawn from the first case in each group, which is only honest when every case in the group shares it.
headerRowsHow many of the tile's top rows form the group heading. Those rows are drawn once per group, from the group's first case; the rows below are drawn for every case. The line must be a clean cut — at least one field above it, at least one below it, and no field crossing it.

Two consequences to pass on to whoever asked for grouping. Choosing a group uses its first case. A several-case workflow can choose several groups, but each group's one checkbox still adds only its first case; the other rows are there to read. Every case with no such connection lands together in one group. The list also pages by group, so a page holds whole groups and however many cases they carry.

Case search is the search experience that surrounds the case list: the screen title, helper text, and search button label the user sees, plus a few niche search-side filters most authors never reach for. These tools set those slots; the search inputs themselves live on the case list above. Each setting is independent: pass null on any slot to clear it.

ToolWhat it does
set_case_search_displaySet the search-screen title, subtitle, search button label, and the predicate that decides when to show the search button.
set_case_search_advancedSet the advanced cluster: an expression naming owner ids whose cases are excluded from search results.

Working on forms

CommCare has four form types: registration creates a new case, followup updates a case, close loads and closes a case, survey is standalone (no case).

ToolWhat it does
create_formAdd a new form to moduleUuid together with its fields, in one call. It returns the form plus every field and inline-choice UUID; predeclare only identities referenced inside the call.
update_formRename, refine one participant's CommCare Connect config after a mode exists, or set close condition/post-submit destination, addressed by moduleUuid and formUuid.
remove_formDelete a form by moduleUuid and formUuid.
get_formRead one form by moduleUuid and formUuid, including its field UUIDs, complete case-operation ASTs, and its after-submit links in the order they are checked (each link's uuid is the linkUuid the link tools take).

Where a form goes after submit

A form's post_submit says where people go once it is submitted. When the destination depends on data, give the form after-submit links: they are checked in order after the form closes and the first true condition is followed. An unconditional link is the otherwise and can only be last; with conditional links and no otherwise link, post_submit is where people go when nothing matches, and Commcare Nova stores it explicitly if it was not set. A link's condition and datums run after the form has closed, so they read the module's case, worker, and session values, never the form's answers; save an answer to a case property first. A link that names datums must name every selection datum the target needs. Each link has an immutable identity: get_form returns it as the link's uuid, and the link tools take that value as linkUuid.

ToolWhat it does
add_form_linksAdd one or more complete links to moduleUuid and formUuid, in the order they are checked. Predeclare linkUuid only when you need the handle before reading it back; afterLinkUuid is the existing anchor (null puts the block first, omission lets Commcare Nova place it).
update_form_linkReplace one link's complete shape by linkUuid. Only changed slots are written; removing the condition makes it the otherwise link, which must already be last.
move_form_linkMove linkUuid after afterLinkUuid, or pass null to check it first. Refuses an order that puts a conditional link after the otherwise link, or the otherwise link anywhere but last.
remove_form_linkRemove one link by linkUuid. Removing the otherwise link while conditional links remain stores post_submit explicitly as the fallback.

Working with Project data

Project data belongs to the app's Nova Project, so a change may affect several apps. Read before writing. get_lookup_tables returns table and column definitions without the row values, plus counts, byte use, and the current Project, definition, row, and table revisions. Every table id and column id is its immutable UUID. Names, tags, labels, and export names are included so the agent can explain its choice, not so it can address the resource.

get_lookup_table_rows reads up to 100 rows at a time in authored order. Each page is bound to the table revision, query, and requested columns; if the table changes between pages, restart the read instead of combining two generations. Repeat the same query and columnIds with each returned cursor. Rows and cells use stable row and column UUIDs. A missing cell and an explicitly empty text value are different values and stay different across every tool.

Every write other than table creation takes expectedTableRevision. Chain the returned revision into the next write, and re-read after a conflict. Adds and moves use afterColumnId or afterRowId; null means first and an omitted add anchor means append. Numeric positions, table tags, and column export names are never addresses.

create_lookup_table creates a table, its columns, and optional initial rows in one transaction. Request-local column keys let initial row cells name columns whose UUIDs do not exist yet. The result pairs each key with its minted column UUID and returns each row UUID by input position, without inventing a second durable identity. edit_lookup_columns batches add, update, move, remove, and retype operations. edit_lookup_rows batches add, complete-row update, move, and remove operations. replace_lookup_rows replaces the complete row set and mints new row UUIDs for the replacement. A batch either lands completely or saves nothing.

Changing a table or column export name, retyping or removing a column, and removing a table require admin access. Other writes require edit access. A referenced table or column cannot be removed, incompatible values block a retype, and the final column cannot be removed. A stale write returns the current revision, and a destructive refusal states whether an app or accepted design still depends on the resource; there is no force option.

set_field_options_source replaces one single- or multiple-choice field's complete source. A lookup source names the table, saved-value column, and display column by UUID. An inline source carries at least two options, each with its own UUID, value, and label. Switching source kinds is a complete replacement; the previous source is not retained.

An optional row filter may compare columns from that same table with fixed values, worker/session values, and eligible earlier answers from the form. It cannot read case data, Search answers, later form answers, or answers inside a child or sibling repeating section.

ToolWhat it does
get_lookup_tablesList this app Project's table definitions without row values, including stable column UUIDs, counts, byte use, and revision axes.
get_lookup_table_rowsRead a snapshot-bound page of one table's ordered rows, optionally narrowed by text query or column projection.
create_lookup_tableCreate a complete table schema plus optional initial rows atomically and return every minted table, column, and row UUID.
update_lookup_tableChange one table's display name or export tag at the expected table revision.
edit_lookup_columnsApply one atomic batch of UUID-addressed add, update, move, remove, or retype column operations.
edit_lookup_rowsApply one atomic batch of UUID-addressed add, whole-row update, move, or remove row operations.
replace_lookup_rowsReplace every row atomically from structured values, returning the replacement row UUIDs.
remove_lookup_tableRemove an unreferenced table at the expected revision.
set_field_options_sourceReplace a choice field's complete inline or lookup source, including an optional lookup row filter, addressed by moduleUuid, formUuid, and fieldUuid.

Changing cases on submission

Most forms save their answers onto their primary case through their fields. Case operations cover additional ordered effects: create another case, update or close a known case, connect cases, rename or retype one, assign an owner, or repeat an effect for every row in a repeating section.

Each tool names the form by moduleUuid and formUuid, both returned by read tools. An operation has an immutable operationUuid and a separate editable id; tools address and reference the operation by UUID. Form-answer terms use field UUIDs, repeat scopes use repeat-field UUIDs, and an operation target uses { "kind": "op", "opUuid": "..." }. A later operation in one add call may consume an earlier create by UUID when the producer declares operationUuid; the complete list is checked and saved atomically.

Operation ids and link identifiers contain letters, numbers, and underscores and start with a letter or underscore. Write-property names use the same characters and start with a letter. These are readable, editable values rather than object addresses: reads return the complete canonical Predicate and ValueExpression ASTs, including lookup-table terms, and subsequent calls keep targeting the operation by operationUuid.

Operation writes may save custom case properties or the standard external_id scalar. They may not write case_name; create, rename, and retype operations expose their dedicated naming fields instead. Commcare Nova keeps standard scalars out of custom case-property data.

ToolWhat it does
get_case_operationsList every operation in execution order with operationUuid, readable metadata, and the complete canonical Predicate/ValueExpression ASTs.
add_case_operationsAdd one or more complete create, update, or close operations. Predeclare operationUuid for same-call references; afterOperationUuid is the existing anchor (null means first, omission appends).
update_case_operationEdit one operation's complete author shape by operationUuid. Only changed identity-keyed slots are written, so unrelated concurrent edits compose.
move_case_operationMove operationUuid after afterOperationUuid, or pass null to make it first. Refuses dependency-breaking or non-portable order.
remove_case_operationRemove one operation by operationUuid. Refuses while another operation still depends on it and names the dependents.

Working on fields (questions)

A field is a single question on a form: text input, dropdown, date, number, repeat group, label, and so on. Most fields also save to a case property; some (on survey forms, or label-only fields) don't.

ToolWhat it does
add_fieldsInsert one or more fields in one call and receive every field and inline-choice UUID in input/source order. Address the form by moduleUuid and formUuid; predeclare fieldUuid for a same-call reference, use parentUuid for nesting, and use beforeFieldUuid / afterFieldUuid for placement.
edit_fieldChange a field by moduleUuid, formUuid, and fieldUuid: label, validation, visibility, choices, or case wiring. It also converts a field's kind in place where the data is compatible, text to dropdown (pass the choices in the same call), text to hidden (pass the calculate), text to barcode, keeping identity and references, and returns the final inline-choice UUIDs when it replaces or creates them. For these string-compatible conversions the collected case data stays valid as-is. Converting a case-bound field is property-wide: the one call also converts the property's other writers in other forms and updates its declared type, so every form stays in agreement. Switching between single- and multi-select changes the shape of stored values and does not migrate existing case data.
move_fieldReposition fieldUuid with beforeFieldUuid / afterFieldUuid, or append under parentUuid (null means the form root; a group, repeat, or section otherwise), keeping every reference intact.
set_form_sectionsSplit a form into sections (pages), re-page it, or return it to a single page, in one call. Pass the complete list of pages, first page first, each naming its top-level questions in order: keep a section by sectionUuid, leave it out to create one, and leave a current section out to remove it once its questions have moved. An empty list removes every section.
remove_fieldDelete a field by moduleUuid, formUuid, and fieldUuid.
get_fieldRead one field by moduleUuid, formUuid, and fieldUuid.
search_blueprintSearch across the whole app for a field id, case property, label, case type, XPath fragment, or module/form name. Returns at most 50 matches. When there are more, the result says how many and your agent should search something more specific.
rename_case_propertiesRename one or more case properties everywhere in the app as one simultaneous, lossless change.

A section is a field kind of its own: a page of the form. On a phone each section is one screen, Next checks that screen before moving on, and an empty or fully hidden page is skipped. Once a form has a section, every top-level field is one, so a new sectioned form is best authored in one add_fields call (the sections first, with predeclared fieldUuids; each question with parentUuid naming its section), and an existing form is re-paged with set_form_sections rather than a run of move_field calls, which the commit gate would refuse half-way. A worker-added repeat cannot sit on a page; fixed-count and case-query repeats can.

Saving a field answer to a case

Every case-writing field carries one complete caseWrite destination. The field's id remains its friendly form-local name, including in human XPath such as #form/first_name; it does not need to match the saved property:

{
  "app_id": "app-id",
  "moduleUuid": "01890f45-0000-7000-8000-000000000001",
  "formUuid": "01890f45-0000-7000-8000-000000000002",
  "fields": [
    {
      "id": "first_name",
      "kind": "text",
      "caseWrite": {
        "caseType": "patient",
        "property": "given_name"
      }
    }
  ]
}

The same field shape is used inside create_form and create_module. On creation, omit caseWrite or pass null for a field that should not save to a case. In edit_field.updates, pass a complete pair to retarget the writer, pass null to stop writing a case property, or leave caseWrite out to keep it. Changing id never renames saved case data.

An attachment field (image, audio, video, signature, file) saves to a case too, and its caseWrite carries one extra member naming what reaches the case:

{
  "id": "site_photo",
  "kind": "image",
  "caseWrite": {
    "caseType": "patient",
    "property": "photo_url",
    "mode": "url"
  }
}

mode is required on those five kinds and refused on every other kind. "url" saves a link to the captured file, built from the CommCare project space the app is published to; an artifact compiled before the app reaches one omits the property and compile_app says so in its advisories. An attachment may not write case_name or external_id.

A field can also save to the worker's own record instead of a case, by passing caseType: "commcare-user" and a property that is one of the app's declared worker property slugs (get_users lists them):

{
  "id": "visits_so_far",
  "kind": "text",
  "caseWrite": { "caseType": "commcare-user", "property": "visits_done" }
}

This works on any form, including a survey with no case type of its own, and the value reads back as #user/visits_done. Three destinations are refused, each for a reason the refusal names: a slug no worker property declares, a built-in worker field (username, first_name, last_name, language, phone_number, hq_user_id, case_name, and the commcare_* keys), which Commcare Nova keeps in step with the worker's profile, and any field inside a repeat, since one form writes one worker record. Adding a worker property is add_user_properties.

Custom property names remain open. The two standard scalar destinations a field may use are case_name and external_id; other reserved system names are refused. These scalars store text: use a text field for an entered answer or a hidden calculated field for a computed value. Commcare Nova trims only boundary characters U+0000 through U+0020 and limits the result to 255 UTF-16 units. A blank case name is invalid. An active blank external ID clears it, while an omitted or irrelevant writer leaves the existing external ID unchanged.

Renaming case properties

rename_case_properties takes the complete nonempty relation for one app-wide change:

{
  "app_id": "app-id",
  "renames": [
    {
      "caseType": "patient",
      "from": "phone",
      "to": "primary_phone"
    },
    {
      "caseType": "patient",
      "from": "primary_phone",
      "to": "phone"
    }
  ]
}

All entries happen simultaneously, so the example is a swap rather than two overwrites. Chains and cycles are also valid. Each source and destination must be unique within its case type; merges, self-renames, temporary-property sequences, and overwriting an occupied destination are refused with nothing saved. Case properties are the intentional name-keyed exception to Commcare Nova's UUID object addresses, so each entry uses the exact (caseType, from, to) names, never a form id, field path, field UUID, or slug.

On success, the result returns the exact renames relation and a grouped document impact:

{
  "message": "Renamed 2 case properties across 6 document occurrences in 5 carriers as one simultaneous app-wide change.",
  "renames": [
    {
      "caseType": "patient",
      "from": "phone",
      "to": "primary_phone"
    },
    {
      "caseType": "patient",
      "from": "primary_phone",
      "to": "phone"
    }
  ],
  "impact": {
    "totalOccurrences": 6,
    "totalCarriers": 5,
    "groups": [
      { "key": "field-writers", "occurrences": 2, "carriers": 2 },
      { "key": "case-operation-writes", "occurrences": 1, "carriers": 1 },
      { "key": "typed-reads", "occurrences": 1, "carriers": 1 },
      { "key": "catalog-declarations", "occurrences": 2, "carriers": 2 }
    ],
    "byRename": [
      {
        "caseType": "patient",
        "from": "phone",
        "to": "primary_phone",
        "occurrences": 3
      },
      {
        "caseType": "patient",
        "from": "primary_phone",
        "to": "phone",
        "occurrences": 3
      }
    ]
  }
}

Working with media

You can attach images, audio, and video to parts of an app, a field's label or hint, a dropdown choice, a module or form menu tile, or the app logo. First upload the files, then attach them by their asset id. Every attach checks the asset before anything saves: it must be in your library, fully uploaded, and the right type for the slot (an audio file can't become a menu icon), and the app's total attached media must stay within the export limit. A bad id fails the call with a message saying what to fix, and nothing is saved.

ToolWhat it does
upload_media_assetUpload an image, audio, or video file (sent as base64) and get back its asset id. Media libraries are per-Project: pass project_id to upload into a shared Project's library (use the Project of the app that will reference it); omit it for your personal Project. Audio must be .mp3 or .wav and video .mp4, CommCare HQ can't accept .m4a or .ogg.
list_media_assetsList your uploaded files and their asset ids, how your agent finds the ids the attach tools need.
remove_media_assetDelete a file from your library. Refuses if any app still uses it, including an app in its restore window.
attach_field_mediaSet or clear the image/audio/video on field labels, hints, help, or validation messages, one call batches attachments across fields and forms.
attach_option_mediaSet or clear the media on dropdown choices (single- or multi-select), a whole picture-choice field in one call.
set_menu_mediaSet or clear menu icons and audio labels on module home-screen tiles and form menu tiles: any mix of both, the whole app's menu in one call.
set_app_logoSet or clear the app logo shown on the login and home screens.

Publishing

ToolWhat it does
compile_appCompile to CommCare HQ format: the JSON shape HQ accepts (a zip bundling that JSON with the media files and the lookup-tables.xlsx workbook when the app has either), or a .ccz archive. When the app needs destination support, a separate project-space compatibility block lists it as not checked without blocking the download.
get_hq_connectionCheck whether you've added an HQ API key in Commcare Nova settings, and if so, which CommCare server it belongs to (US, India, or EU, separate servers with separate accounts) and which project spaces (domains) it can reach.
upload_app_to_hqPush the app to CommCare HQ: its lookup tables and its organization's places first, then the app itself. The first upload to a project space creates the app there; uploading again updates that same HQ app in place (hq_app_action in the result says which happened). Pass domain to choose the target space when your key reaches several. The tool rechecks project-space compatibility immediately before remote writes and returns project_space_compatibility, the HQ app URL, the deployment state, and the setup steps the project space still needs by hand.
get_deploymentReport every project space the app has been published to and how far along each one is: preflight, resources, uploaded, built, released, runnable, or incomplete. Reads Commcare Nova's own record, without contacting CommCare HQ.
refresh_deploymentAsk CommCare HQ again what has happened to a published app, and update the stored state. Read-only against CommCare HQ. It can move a deployment forward, and can move it back when a build stops being released there.
provision_workersCreate CommCare mobile-worker accounts on a project space for the app's personas, or bring existing ones into step with what the app says. The app must already be published there. Each new account carries that persona's worker information and places, and comes back with a password that exists only in that answer.

An upload never changes your Commcare Nova copy. If the linked HQ app was deleted on HQ, the upload refuses with remote_app_missing and tells you that uploading again will create a fresh one. The deployment record names anything an earlier upload left behind, under left_behind, so you can archive or delete it on HQ. Commcare Nova never deletes a resource it put on a project space.

A shared name is not permission to write over anything. An app's lookup tables and its organization's places are pushed before the app, because the app reaches for them by name at runtime. HQ matches a lookup table on its export tag and a place on its site code, so if the project space already holds one under a name this app needs and Commcare Nova did not create it, the upload refuses with hq_resource_conflict and sends nothing. The envelope's resource_conflicts names each one: kind is lookup-table or location, name is what the user calls it in Commcare Nova, hq_name is what they will see on HQ. Ask the user about each one by name and never decide for them. Re-call with adopt_resources carrying the nova_resource_id of exactly the ones they confirmed are theirs; those are then kept in step with Commcare Nova on every upload. Anything they did not name stays untouched. The way out differs by kind: rename the table's export tag in Project data, or, because a place's site code is set once, remove the place in Organization and add it again with a code that is free.

Commcare Nova cannot create organization levels, and its places have to fit yours. HQ's location API is read-only for levels, and it takes a place only at the level directly below its parent's, with sibling names that differ. If the app's places do not fit, the upload refuses with hq_organization_mismatch and sends nothing; the message names each place and what has to change. No adoption resolves this one — the fix is in Commcare Nova's Organization or on HQ's Organization Levels page. The upload result's setup steps carry every level the app needs, in the order to create them.

Places go out in groups, and a group is all or nothing. HQ takes at most 100 places per atomic request, so Commcare Nova sends one group per level, working down. If a group is refused, the groups above it are already on the project space and are recorded as such; uploading again carries on from there rather than making a second copy. Never tell the user nothing was created after a partial upload — call get_deployment and report what the record says.

A password comes back once and exists nowhere else. provision_workers generates a password for every account it creates and hands it back in workers[].password. Commcare Nova stores none of them and cannot show one again — a lost one is reset on HQ. Show every password to the user immediately, before doing anything else with the result, including when the result also carries an error_type: a call that stopped partway still made real accounts, and their passwords are in that same answer. An account that was updated rather than created has password: null, because its person's password is untouched.

An error does not always mean the account was not made. HQ saves a worker before it writes its reply, so a request that breaks off after that point leaves a real account behind and still answers with an error. When that happens provision_workers refuses with hq_worker_may_exist and puts that account in unconfirmed_workers with its password. Show it exactly as urgently as any other password: if the account is there, that password is the only one it will ever have, and Commcare Nova cannot look it up, because HQ's username search runs on a search index that trails a new account by seconds. Tell the user to look for that username on their project space. If it isn't there, call again to make the account. If it is, wait a moment before calling again: HQ's username search trails its own new account by seconds, so until it catches up a retry still tries to create and comes back saying the name is taken, and the take-over path isn't offered yet.

A username that is taken belongs to somebody. A mobile username is the whole address, name@project-space.commcarehq.org, so the same name is free on every other project space and taken only on this one. Here, a name already in use is a real person's account, and retiring that account does not give the name back. provision_workers refuses with hq_worker_conflict, writes nothing, and names each one in worker_conflicts. Ask the user about each account by name and never decide for them. Re-call with adopt_personas carrying the persona_uuid of exactly the ones they confirmed. The other way out is a username nobody has yet — a username is set once when the account is made, so giving a persona a new one makes a second account and leaves the first alone.

Commcare Nova never deletes or retires a worker. HQ's own delete soft-deletes every case that worker owns, so removing a persona reports its account under the deployment's left_behind instead. Removing it is the user's to do on HQ. For the same reason, a persona Commcare Nova already has an account for is always updated rather than remade, even when the account no longer turns up on HQ.

Provisioning refuses before it writes, because HQ would not. CommCareUserResource creates a worker and silently drops a location assignment it cannot resolve, so provision_workers decides everything knowable first and answers workers_not_provisionable with each reason named: a username HQ will not take, worker information the app marks required that a persona has no value for, or a persona standing in a place the project space does not hold yet. The last one is fixed by uploading the app there, which puts the places there. Roles and the project's user-data field definitions are not created either — neither has an API — so they stay in the upload result's setup steps.

Uploading is not the same as releasing. HQ accepts an API key for putting an app on a project space, but making a version and releasing one are only possible from a signed-in browser session. So upload_app_to_hq reaches uploaded, and built, released, and runnable are states Commcare Nova OBSERVES after a person does those steps on HQ. Tell the user to open the app's Releases screen, choose Make new version, then star it; then call refresh_deployment. Never report an uploaded app as released, live, or ready for workers.

A refresh that couldn't ask is not a verdict on the app. refresh_deployment fails rather than answering with a stale record when Commcare Nova cannot reach HQ at all — no API key on the account, a key that no longer reaches that project space, HQ not answering, or an app that was never published there. Nothing on the project space changed, so the last state you saw is still the true one. Report the message, not a failed deployment.

Edits are checked as they happen, and publishing is checked again at the door. A tool call that would break the app, a bad expression, a duplicate name, a reference to something that doesn't exist, or a piece left unfinished (an empty form, a case list with no columns), fails with a message naming each problem, and nothing is saved. Structure always lands whole: create_form requires the form's fields, and create_module requires its forms and case-list columns (with its case type already recorded via generate_schema), so a rejection is always fixable by adjusting the same call. compile_app and upload_app_to_hq refuse (invalid_input) while the app still has unresolved findings, listing each one.

System reminders

A few read results (get_app, get_field) may include a <system_reminder> block. It carries background context for the calling agent, for example, a case property the app reads that no form in it writes, whose values therefore come from outside the app (another app on the same case type, an integration, or sample cases created for testing). A reminder is informational, never an error: nothing needs fixing, and it isn't meant to be relayed to end users.