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
| Tool | What it does |
|---|---|
list_apps | List your apps with optional status filter, sort, and pagination. Each entry names the Nova Project the app lives in (project_id + project_name). |
search_apps | Fuzzy search by name. Case-insensitive, tolerates typos. Entries carry the same Project columns as list_apps. |
get_app | Summarize 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_compatibility | Check 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.
| Tool | What it does |
|---|---|
get_languages | Read the ordered language catalog, source and default languages, per-target coverage, and diagnostics for content the static app-language catalog cannot translate. |
get_translatable_content | Page through the snapshot-bound translation inventory for one language, including source/effective values, status, breadcrumb context, and protected reference parts. |
add_language | Add one target and atomically seed every current unit from an existing copyFrom language. |
update_language | Set the runtime default, or replace the sole source language's identity. |
remove_language | Remove a target language and its overlay. The source language and the final remaining language cannot be removed. |
update_translations | Atomically 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, orblockedfor the checked destinationtarget_domain: the exact project space that was checkedrequired_capabilities: the app features that need project-space support, including why the app uses each one and whether it isavailable,missing, orunverifiedblockers: required capabilities that are missing or could not be confirmedadvisories: guidance that never blocks publishing, including slower large Search results when the faster path is unavailable or unverifiedmessage,support_email, anddocs_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
| Tool | What it does |
|---|---|
create_app | Create 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_app | Soft-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.
| Tool | What it does |
|---|---|
list_projects | List 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_project | Create 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_member | Invite 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_members | List 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_role | Change 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_app | Move 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.
| Tool | What it does |
|---|---|
update_app | Set the app's name. |
configure_connect | Set the exact app-wide CommCare Connect target atomically, or turn Connect off and clear every form block. |
generate_schema | Record 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.
| Tool | What it does |
|---|---|
get_users | Read worker information, roles, and personas, including their stable UUIDs and UUID-linked values. |
add_user_properties | Add one or more worker-information properties. Returns their stable UUIDs. |
update_user_property | Change one property's label, saved name, required flag, or accepted values. |
remove_user_property | Remove 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_types | Add one or more reusable roles and their default worker-information values. |
update_user_type | Change one role's name or description, or set/clear one UUID-addressed default with valuePatch. |
remove_user_type | Remove one role. Refuses while a persona still uses it. |
add_personas | Add one or more named Preview workers, optionally with a role and value overrides. |
update_persona | Change one persona's name, description, or role, or set/clear one UUID-addressed override with valuePatch. |
remove_persona | Remove 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.
| Tool | What it does |
|---|---|
get_organization | Page through the snapshot-bound organization model and matching place rows. |
add_organization_levels | Add levels parent-first and return their stable UUIDs. |
update_organization_level | Change one level's editable settings by UUID. |
remove_organization_level | Remove an unused level by UUID. |
add_location_properties | Add UUID-addressed place-information declarations. |
update_location_property | Change one property's declaration, including its complete level applicability. |
remove_location_property | Remove one property and its stored values after reference checks. |
create_location | Create one place and, when required, one bounded structurally nested descendant tree atomically. |
update_location | Change one place's editable data, retype it, or set/clear one UUID-keyed custom value. |
move_location | Move one place within the valid level topology while preserving its UUID. |
set_location_archived | Preflight 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.
| Tool | What it does |
|---|---|
get_automations | Read every automation in display order, including stable nested UUIDs, regenerated setup guidance, and an explicit statement that Preview does not execute it. |
add_automations | Add 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_automation | Set 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_automation | Remove 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.
| Tool | What it does |
|---|---|
create_module | Add 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_module | Rename 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_module | Delete a module by moduleUuid. A parent must have no child modules before it can be removed. |
move_module | Reorder 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_module | Read 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.
| Tool | What it does |
|---|---|
configure_case_list | Configure 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_columns | Add 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_column | Replace a column's body, addressed by moduleUuid and columnUuid. |
remove_case_list_column | Remove one column by moduleUuid and columnUuid. |
reorder_case_list_columns | Reorder a surface by passing its complete columnUuids sequence. |
set_case_list_filter | Set or clear the optional filter predicate that narrows the list. |
add_search_inputs | Add 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_input | Replace a search input's body, addressed by moduleUuid and searchInputUuid. |
remove_search_input | Remove one search input by moduleUuid and searchInputUuid. |
reorder_search_inputs | Reorder search inputs by passing their complete searchInputUuids sequence. |
configure_case_selection | Pass 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_tile | Lay 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:
| Field | What it does |
|---|---|
identifier | The 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. |
headerRows | How 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.
Working on case search
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.
| Tool | What it does |
|---|---|
set_case_search_display | Set the search-screen title, subtitle, search button label, and the predicate that decides when to show the search button. |
set_case_search_advanced | Set 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).
| Tool | What it does |
|---|---|
create_form | Add 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_form | Rename, refine one participant's CommCare Connect config after a mode exists, or set close condition/post-submit destination, addressed by moduleUuid and formUuid. |
remove_form | Delete a form by moduleUuid and formUuid. |
get_form | Read 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.
| Tool | What it does |
|---|---|
add_form_links | Add 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_link | Replace 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_link | Move 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_link | Remove 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.
| Tool | What it does |
|---|---|
get_lookup_tables | List this app Project's table definitions without row values, including stable column UUIDs, counts, byte use, and revision axes. |
get_lookup_table_rows | Read a snapshot-bound page of one table's ordered rows, optionally narrowed by text query or column projection. |
create_lookup_table | Create a complete table schema plus optional initial rows atomically and return every minted table, column, and row UUID. |
update_lookup_table | Change one table's display name or export tag at the expected table revision. |
edit_lookup_columns | Apply one atomic batch of UUID-addressed add, update, move, remove, or retype column operations. |
edit_lookup_rows | Apply one atomic batch of UUID-addressed add, whole-row update, move, or remove row operations. |
replace_lookup_rows | Replace every row atomically from structured values, returning the replacement row UUIDs. |
remove_lookup_table | Remove an unreferenced table at the expected revision. |
set_field_options_source | Replace 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.
| Tool | What it does |
|---|---|
get_case_operations | List every operation in execution order with operationUuid, readable metadata, and the complete canonical Predicate/ValueExpression ASTs. |
add_case_operations | Add 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_operation | Edit one operation's complete author shape by operationUuid. Only changed identity-keyed slots are written, so unrelated concurrent edits compose. |
move_case_operation | Move operationUuid after afterOperationUuid, or pass null to make it first. Refuses dependency-breaking or non-portable order. |
remove_case_operation | Remove 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.
| Tool | What it does |
|---|---|
add_fields | Insert 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_field | Change 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_field | Reposition 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_sections | Split 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_field | Delete a field by moduleUuid, formUuid, and fieldUuid. |
get_field | Read one field by moduleUuid, formUuid, and fieldUuid. |
search_blueprint | Search 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_properties | Rename 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.
| Tool | What it does |
|---|---|
upload_media_asset | Upload 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_assets | List your uploaded files and their asset ids, how your agent finds the ids the attach tools need. |
remove_media_asset | Delete a file from your library. Refuses if any app still uses it, including an app in its restore window. |
attach_field_media | Set or clear the image/audio/video on field labels, hints, help, or validation messages, one call batches attachments across fields and forms. |
attach_option_media | Set or clear the media on dropdown choices (single- or multi-select), a whole picture-choice field in one call. |
set_menu_media | Set 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_logo | Set or clear the app logo shown on the login and home screens. |
Publishing
| Tool | What it does |
|---|---|
compile_app | Compile 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_connection | Check 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_hq | Push 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_deployment | Report 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_deployment | Ask 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_workers | Create 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.