Document Inspection Starter Pack

Inspect document metadata by URI or straight from the node, run against llamaverse today

personClever Llamas
CleverLlamasMinimum Llamaverse Version: 2
databaseMinimum MarkLogic Version: 11

URI, kind, collection, and permission outputs all resolve to the same source document, whether you start from the document itself or from a node buried three levels inside it.

This starter pack is for those moments when you need to inspect a document quickly without writing half a framework first. It keeps the output focused on the metadata that explains how a document is classified, protected, and addressed in the database - and it draws a hard line between two function families that get confused constantly: xdmp:document-xxx, which needs a URI, and xdmp:node-xxx, which needs nothing but the node.

The examples in this article assume the llamaverse (v2.0+) is deployed. The llamaverse sample data is freely available from github.com/cleverllamas/llamaverse - see the llamaverse article for full setup instructions. Every example in this pack runs against the same wild-llama document - one with a relatedTo pedigree entry - so URI, kind, collection, and permission output can be compared side by side against the same underlying JSON payload.

API Context for This Pack

Context ItemWhat this pack uses
Data setllamaverse v2.0+ sample data
Primary document scopewild llama documents addressed by URI, and nodes addressed directly
Primary focusdocument metadata inspection (URI, kind, collections, permissions, properties, JSON metadata)
Views usedNone
Index contextMetadata access via xdmp document and node APIs rather than TDE or Optic view projection

If outputs look inconsistent, verify the URI exists in the target database and that your current user can read document metadata.

Two Families: xdmp:document-xxx vs xdmp:node-xxx

Both families answer the same kinds of questions - what collections is this in, what permissions protect it - but they start from different places, and mixing them up costs people real debugging time.

xdmp:document-xxxxdmp:node-xxx
InputA document URI (xs:string)Any node - document, element, attribute, text, whatever you have in hand
Lookup required first?Yes - you must already know or resolve the URINo - works directly on a node you already hold, at any depth
Typical useYou have a URI (from cts:uris(), a search result, an app parameter) and want facts about that documentYou're already holding a node mid-query - a matched element, a deeply nested value - and want facts about its owning document without a separate lookup
Examples in this packxdmp:document-get-collections(), xdmp:document-get-permissions(), xdmp:document-properties(), xdmp:document-get-metadata() / xdmp:document-set-metadata()xdmp:node-uri(), xdmp:node-kind(), xdmp:node-collections(), xdmp:node-permissions(), xdmp:node-metadata() / xdmp:node-metadata-value()

The xdmp:node-xxx family exists because MarkLogic stores documents as fragments, and every node in a fragment - no matter how deeply nested - already knows which fragment owns it. You don't need to walk back up to the root and extract a URI before asking these questions; the node answers them directly. The examples below prove this by reaching three levels into a document (past the document node, past a relatedTo object, down to a relationship text node) and showing that xdmp:node-uri(), xdmp:node-kind(), xdmp:node-collections(), and xdmp:node-permissions() all resolve correctly from that single deep node - with the same collections and permissions that xdmp:document-get-collections() and xdmp:document-get-permissions() return for the document's URI.

Functions in This Pack

xdmp:node-uri

If a node had a passport, this function is border control - and it doesn't matter how deep in the document that node is standing.

Source docs: https://docs.marklogic.com/11.0/xdmp:node-uri

Option / ArgumentWhat it controlsUsed here
$nodeNode whose owning document URI should be returnedThe relationship text node, three levels inside the sample document
{
  "id": "b3591799-54a8-44ea-87a1-d1c05659f781",
  "name": "Dana",
  "heightCm": 161,
  "weightKg": 144,
  "eyeColor": "Hazel",
  "hairColor": "Blonde",
  "breed": "Hybrid",
  "placeOfBirth": "Peterchester, Falkland Islands (Malvinas)",
  "description": "Dana is a hazel-eyed llama with blonde hair, standing 161 cm tall. Originally from Peterchester, Falkland Islands (Malvinas), Dana enjoys gardening, painting, hiking. Known for their calm and gentle personality, Dana is a beloved member of the llama community.",
  "relatedTo": {
    "id": "2233b726-9102-4e33-904e-d1fc5a805613",
    "relationship": "15th cousin 9th removed"
  },
  "classificationSpeciesKey": "Lama_glama_vellifer"
}
xquery version "1.0-ml";

(: Pick a wild llama that records a "relatedTo" pedigree entry, then reach three
 : levels deep into that entry - past the document node, past the "relatedTo"
 : object, all the way to the "relationship" text node. :)
let $sample-doc := cts:search(
  collection("wild-llamas"),
  cts:json-property-scope-query("relatedTo", cts:true-query())
)[1]
let $deep-node := $sample-doc/relatedTo/relationship
return
  if (empty($deep-node)) then
    error(xs:QName("NO-LLAMAVERSE"), "No wild-llamas document with a 'relatedTo' entry was found.")
  else
    (: xdmp:node-uri() takes no URI as input - it takes the node itself, no
     : matter how deeply nested, and resolves the URI of the fragment that owns it. :)
    xdmp:node-uri($deep-node)
'use strict';

// Pick a wild llama that records a "relatedTo" pedigree entry, then reach
// three levels deep into that entry - past the document node, past the
// "relatedTo" object, all the way to the "relationship" text node.
const sampleDoc = cts.search(cts.andQuery([
  cts.collectionQuery('wild-llamas'),
  cts.jsonPropertyScopeQuery('relatedTo', cts.trueQuery())
])).toArray()[0];

if (!sampleDoc) {
  throw new Error("NO-LLAMAVERSE: No wild-llamas document with a 'relatedTo' entry was found.");
}

const deepNode = sampleDoc.xpath('/relatedTo/relationship');

// xdmp.nodeUri() takes no URI as input - it takes the node itself, no matter
// how deeply nested, and resolves the URI of the fragment that owns it.
xdmp.nodeUri(deepNode);
/cleverllamas/llamaverse/raw/wild-llamas/llamas/b3591799-54a8-44ea-87a1-d1c05659f781.json
LineValue
1/cleverllamas/llamaverse/raw/wild-llamas/llamas/b3591799-54a8-44ea-87a1-d1c05659f781.json

What to notice: the input was a text node nested inside relatedTo, not the document itself, yet xdmp:node-uri() still resolves the exact same URI you'd get from the document node. No URI lookup, no path walking - the node already knows its fragment.

xdmp:node-kind

Before you branch logic on a node, know what kind of node you're holding.

Source docs: https://docs.marklogic.com/11.0/xdmp:node-kind

Option / ArgumentWhat it controlsUsed here
$nodeNode whose kind should be classifiedThe same relationship text node
{
  "id": "b3591799-54a8-44ea-87a1-d1c05659f781",
  "name": "Dana",
  "heightCm": 161,
  "weightKg": 144,
  "eyeColor": "Hazel",
  "hairColor": "Blonde",
  "breed": "Hybrid",
  "placeOfBirth": "Peterchester, Falkland Islands (Malvinas)",
  "description": "Dana is a hazel-eyed llama with blonde hair, standing 161 cm tall. Originally from Peterchester, Falkland Islands (Malvinas), Dana enjoys gardening, painting, hiking. Known for their calm and gentle personality, Dana is a beloved member of the llama community.",
  "relatedTo": {
    "id": "2233b726-9102-4e33-904e-d1fc5a805613",
    "relationship": "15th cousin 9th removed"
  },
  "classificationSpeciesKey": "Lama_glama_vellifer"
}
xquery version "1.0-ml";

(: Same wild llama used throughout this pack - the one with a "relatedTo"
 : pedigree entry. :)
let $sample-doc := cts:search(
  collection("wild-llamas"),
  cts:json-property-scope-query("relatedTo", cts:true-query())
)[1]
let $deep-node := $sample-doc/relatedTo/relationship
return
  if (empty($deep-node)) then
    error(xs:QName("NO-LLAMAVERSE"), "No wild-llamas document with a 'relatedTo' entry was found.")
  else
    (: xdmp:node-kind() classifies any node - document, element, attribute,
     : text, and so on - without needing to know its URI first. :)
    xdmp:node-kind($deep-node)
'use strict';

// Same wild llama used throughout this pack - the one with a "relatedTo"
// pedigree entry.
const sampleDoc = cts.search(cts.andQuery([
  cts.collectionQuery('wild-llamas'),
  cts.jsonPropertyScopeQuery('relatedTo', cts.trueQuery())
])).toArray()[0];

if (!sampleDoc) {
  throw new Error("NO-LLAMAVERSE: No wild-llamas document with a 'relatedTo' entry was found.");
}

const deepNode = sampleDoc.xpath('/relatedTo/relationship');

// xdmp.nodeKind() classifies any node - document, element, attribute, text,
// and so on - without needing to know its URI first.
xdmp.nodeKind(deepNode);
text
LineValue
1text

What to notice: xdmp:node-kind() returns one of document, element, attribute, text, namespace, processing-instruction, binary, or comment. Here it correctly identifies a JSON leaf value as a text node - useful when code needs to branch on node shape before processing it.

xdmp:node-collections

The node-level counterpart to xdmp:document-get-collections - same answer, no URI required.

Source docs: https://docs.marklogic.com/11.0/xdmp:node-collections

Option / ArgumentWhat it controlsUsed here
$nodeNode whose owning document's collections should be returnedThe same relationship text node
{
  "id": "b3591799-54a8-44ea-87a1-d1c05659f781",
  "name": "Dana",
  "heightCm": 161,
  "weightKg": 144,
  "eyeColor": "Hazel",
  "hairColor": "Blonde",
  "breed": "Hybrid",
  "placeOfBirth": "Peterchester, Falkland Islands (Malvinas)",
  "description": "Dana is a hazel-eyed llama with blonde hair, standing 161 cm tall. Originally from Peterchester, Falkland Islands (Malvinas), Dana enjoys gardening, painting, hiking. Known for their calm and gentle personality, Dana is a beloved member of the llama community.",
  "relatedTo": {
    "id": "2233b726-9102-4e33-904e-d1fc5a805613",
    "relationship": "15th cousin 9th removed"
  },
  "classificationSpeciesKey": "Lama_glama_vellifer"
}
xquery version "1.0-ml";

(: Same wild llama used throughout this pack - the one with a "relatedTo"
 : pedigree entry. :)
let $sample-doc := cts:search(
  collection("wild-llamas"),
  cts:json-property-scope-query("relatedTo", cts:true-query())
)[1]
let $deep-node := $sample-doc/relatedTo/relationship
return
  if (empty($deep-node)) then
    error(xs:QName("NO-LLAMAVERSE"), "No wild-llamas document with a 'relatedTo' entry was found.")
  else
    (: xdmp:node-collections() asks the node itself for its document's
     : collections - no URI lookup step required. :)
    xdmp:node-collections($deep-node)
'use strict';

// Same wild llama used throughout this pack - the one with a "relatedTo"
// pedigree entry.
const sampleDoc = cts.search(cts.andQuery([
  cts.collectionQuery('wild-llamas'),
  cts.jsonPropertyScopeQuery('relatedTo', cts.trueQuery())
])).toArray()[0];

if (!sampleDoc) {
  throw new Error("NO-LLAMAVERSE: No wild-llamas document with a 'relatedTo' entry was found.");
}

const deepNode = sampleDoc.xpath('/relatedTo/relationship');

// xdmp.nodeCollections() asks the node itself for its document's collections
// - no URI lookup step required.
Sequence.from(xdmp.nodeCollections(deepNode));
wild-llamas
raw
LineValue
1wild-llamas
2raw

What to notice: these are the exact same two collections that xdmp:document-get-collections() returns further down for this document's URI. Reaching the node directly skips a URI lookup step entirely - useful mid-query, when you already have the node and don't want a second round trip.

xdmp:node-permissions

The node-level counterpart to xdmp:document-get-permissions - and the same role-id-to-name resolution problem shows up here too.

Source docs: https://docs.marklogic.com/11.0/xdmp:node-permissions

Option / ArgumentWhat it controlsUsed here
$nodeNode whose owning document's permissions should be returnedThe same relationship text node
{
  "id": "b3591799-54a8-44ea-87a1-d1c05659f781",
  "name": "Dana",
  "heightCm": 161,
  "weightKg": 144,
  "eyeColor": "Hazel",
  "hairColor": "Blonde",
  "breed": "Hybrid",
  "placeOfBirth": "Peterchester, Falkland Islands (Malvinas)",
  "description": "Dana is a hazel-eyed llama with blonde hair, standing 161 cm tall. Originally from Peterchester, Falkland Islands (Malvinas), Dana enjoys gardening, painting, hiking. Known for their calm and gentle personality, Dana is a beloved member of the llama community.",
  "relatedTo": {
    "id": "2233b726-9102-4e33-904e-d1fc5a805613",
    "relationship": "15th cousin 9th removed"
  },
  "classificationSpeciesKey": "Lama_glama_vellifer"
}
xquery version "1.0-ml";

(: Same wild llama used throughout this pack - the one with a "relatedTo"
 : pedigree entry. :)
let $sample-doc := cts:search(
  collection("wild-llamas"),
  cts:json-property-scope-query("relatedTo", cts:true-query())
)[1]
let $deep-node := $sample-doc/relatedTo/relationship
return
  if (empty($deep-node)) then
    error(xs:QName("NO-LLAMAVERSE"), "No wild-llamas document with a 'relatedTo' entry was found.")
  else
    (: xdmp:node-permissions() reads permissions straight from the node - the
     : same permissions xdmp:document-get-permissions() would return for its
     : URI. Role IDs are resolved to names with xdmp:role-name(). :)
    for $permission in xdmp:node-permissions($deep-node)
    return
      <permission>
        <role-name>{xdmp:role-name($permission/sec:role-id)}</role-name>
        <capability>{$permission/sec:capability/fn:string()}</capability>
      </permission>
'use strict';

// Same wild llama used throughout this pack - the one with a "relatedTo"
// pedigree entry.
const sampleDoc = cts.search(cts.andQuery([
  cts.collectionQuery('wild-llamas'),
  cts.jsonPropertyScopeQuery('relatedTo', cts.trueQuery())
])).toArray()[0];

if (!sampleDoc) {
  throw new Error("NO-LLAMAVERSE: No wild-llamas document with a 'relatedTo' entry was found.");
}

const deepNode = sampleDoc.xpath('/relatedTo/relationship');

// xdmp.nodePermissions() reads permissions straight from the node - the same
// permissions xdmp.documentGetPermissions() would return for its URI. Role
// IDs are resolved to names with xdmp.roleName().
const permissions = xdmp.nodePermissions(deepNode).map((permission) => ({
  roleName: xdmp.roleName(permission.roleId),
  capability: permission.capability
}));

Sequence.from(permissions);
cleverllamas-llamaverse-writer : update
cleverllamas-llamaverse-writer : node-update
cleverllamas-llamaverse-writer : insert
cleverllamas-llamaverse-reader : read
LineRoleCapability
1cleverllamas-llamaverse-writerupdate
2cleverllamas-llamaverse-writernode-update
3cleverllamas-llamaverse-writerinsert
4cleverllamas-llamaverse-readerread

What to notice: xdmp:node-permissions() returns raw sec:role-id values just like xdmp:document-get-permissions() does - both functions resolve them here with xdmp:role-name() (XQuery) and xdmp.roleName() (JavaScript) so the output reads as roles, not opaque integers.

xdmp:document-get-collections

The quickest way to answer, "which llama bucket did this document land in?" - when you're starting from a URI instead of a node.

Source docs: https://docs.marklogic.com/11.0/xdmp:document-get-collections

Option / ArgumentWhat it controlsUsed here
$uriDocument URI whose collection list is requestedSample wild-llama document URI
{
  "id": "b3591799-54a8-44ea-87a1-d1c05659f781",
  "name": "Dana",
  "heightCm": 161,
  "weightKg": 144,
  "eyeColor": "Hazel",
  "hairColor": "Blonde",
  "breed": "Hybrid",
  "placeOfBirth": "Peterchester, Falkland Islands (Malvinas)",
  "description": "Dana is a hazel-eyed llama with blonde hair, standing 161 cm tall. Originally from Peterchester, Falkland Islands (Malvinas), Dana enjoys gardening, painting, hiking. Known for their calm and gentle personality, Dana is a beloved member of the llama community.",
  "relatedTo": {
    "id": "2233b726-9102-4e33-904e-d1fc5a805613",
    "relationship": "15th cousin 9th removed"
  },
  "classificationSpeciesKey": "Lama_glama_vellifer"
}
xquery version "1.0-ml";

(: Same wild llama used throughout this pack - the one with a "relatedTo"
 : pedigree entry - so every function in this article resolves to the same
 : document. :)
let $sample-doc := cts:search(
  collection("wild-llamas"),
  cts:json-property-scope-query("relatedTo", cts:true-query())
)[1]
let $sample-uri := xdmp:node-uri($sample-doc)
return
  if (empty($sample-uri)) then
    error(xs:QName("NO-LLAMAVERSE"), "No wild-llamas document with a 'relatedTo' entry was found.")
  else
    (
      $sample-uri,
      xdmp:document-get-collections($sample-uri)
    )
'use strict';

// Same wild llama used throughout this pack - the one with a "relatedTo"
// pedigree entry - so every function in this article resolves to the same
// document.
const sampleDoc = cts.search(cts.andQuery([
  cts.collectionQuery('wild-llamas'),
  cts.jsonPropertyScopeQuery('relatedTo', cts.trueQuery())
])).toArray()[0];

if (!sampleDoc) {
  throw new Error("NO-LLAMAVERSE: No wild-llamas document with a 'relatedTo' entry was found.");
}

const sampleUri = xdmp.nodeUri(sampleDoc);
const collections = xdmp.documentGetCollections(sampleUri);

Sequence.from([sampleUri, ...collections]);
/cleverllamas/llamaverse/raw/wild-llamas/llamas/b3591799-54a8-44ea-87a1-d1c05659f781.json
wild-llamas
raw
LineValue
1/cleverllamas/llamaverse/raw/wild-llamas/llamas/b3591799-54a8-44ea-87a1-d1c05659f781.json
2wild-llamas
3raw

What to notice: collection membership confirms the document routing model used by the sample dataset - the same two collections xdmp:node-collections() already returned above, this time reached by URI instead of by node.

xdmp:document-get-permissions

Because production incidents often start with "I thought this role could read that."

Source docs: https://docs.marklogic.com/11.0/xdmp:document-get-permissions

Option / ArgumentWhat it controlsUsed here
$uriDocument URI whose permissions are requestedSample wild-llama document URI
{
  "id": "b3591799-54a8-44ea-87a1-d1c05659f781",
  "name": "Dana",
  "heightCm": 161,
  "weightKg": 144,
  "eyeColor": "Hazel",
  "hairColor": "Blonde",
  "breed": "Hybrid",
  "placeOfBirth": "Peterchester, Falkland Islands (Malvinas)",
  "description": "Dana is a hazel-eyed llama with blonde hair, standing 161 cm tall. Originally from Peterchester, Falkland Islands (Malvinas), Dana enjoys gardening, painting, hiking. Known for their calm and gentle personality, Dana is a beloved member of the llama community.",
  "relatedTo": {
    "id": "2233b726-9102-4e33-904e-d1fc5a805613",
    "relationship": "15th cousin 9th removed"
  },
  "classificationSpeciesKey": "Lama_glama_vellifer"
}
xquery version "1.0-ml";

(: Same wild llama used throughout this pack - the one with a "relatedTo"
 : pedigree entry. :)
let $sample-doc := cts:search(
  collection("wild-llamas"),
  cts:json-property-scope-query("relatedTo", cts:true-query())
)[1]
let $sample-uri := xdmp:node-uri($sample-doc)
return
  if (empty($sample-uri)) then
    error(xs:QName("NO-LLAMAVERSE"), "No wild-llamas document with a 'relatedTo' entry was found.")
  else
    (
      $sample-uri,
      (: Raw permissions only expose a numeric sec:role-id - resolve each one to
       : its role name with xdmp:role-name() so the output is legible. :)
      for $permission in xdmp:document-get-permissions($sample-uri)
      return
        <permission>
          <role-id>{$permission/sec:role-id/fn:string()}</role-id>
          <role-name>{xdmp:role-name($permission/sec:role-id)}</role-name>
          <capability>{$permission/sec:capability/fn:string()}</capability>
        </permission>
    )
'use strict';

// Same wild llama used throughout this pack - the one with a "relatedTo"
// pedigree entry.
const sampleDoc = cts.search(cts.andQuery([
  cts.collectionQuery('wild-llamas'),
  cts.jsonPropertyScopeQuery('relatedTo', cts.trueQuery())
])).toArray()[0];

if (!sampleDoc) {
  throw new Error("NO-LLAMAVERSE: No wild-llamas document with a 'relatedTo' entry was found.");
}

const sampleUri = xdmp.nodeUri(sampleDoc);

// Raw permissions only expose a numeric roleId - resolve each one to its role
// name with xdmp.roleName() so the output is legible.
const permissions = xdmp.documentGetPermissions(sampleUri).map((permission) => ({
  roleId: permission.roleId,
  roleName: xdmp.roleName(permission.roleId),
  capability: permission.capability
}));

Sequence.from([sampleUri, ...permissions]);
/cleverllamas/llamaverse/raw/wild-llamas/llamas/b3591799-54a8-44ea-87a1-d1c05659f781.json
X-Path: /permission
<permission>
<role-id>3611960165157250657</role-id>
<role-name>cleverllamas-llamaverse-writer</role-name>
<capability>update</capability>
</permission>
X-Path: /permission
<permission>
<role-id>3611960165157250657</role-id>
<role-name>cleverllamas-llamaverse-writer</role-name>
<capability>node-update</capability>
</permission>
X-Path: /permission
<permission>
<role-id>3611960165157250657</role-id>
<role-name>cleverllamas-llamaverse-writer</role-name>
<capability>insert</capability>
</permission>
X-Path: /permission
<permission>
<role-id>13627388108119887129</role-id>
<role-name>cleverllamas-llamaverse-reader</role-name>
<capability>read</capability>
</permission>
/cleverllamas/llamaverse/raw/wild-llamas/llamas/b3591799-54a8-44ea-87a1-d1c05659f781.json
{"roleId":"3611960165157250657", "roleName":"cleverllamas-llamaverse-writer", "capability":"update"}
{"roleId":"3611960165157250657", "roleName":"cleverllamas-llamaverse-writer", "capability":"node-update"}
{"roleId":"3611960165157250657", "roleName":"cleverllamas-llamaverse-writer", "capability":"insert"}
{"roleId":"13627388108119887129", "roleName":"cleverllamas-llamaverse-reader", "capability":"read"}
LineRoleCapability
1cleverllamas-llamaverse-writerupdate
2cleverllamas-llamaverse-writernode-update
3cleverllamas-llamaverse-writerinsert
4cleverllamas-llamaverse-readerread

What to notice: raw xdmp:document-get-permissions() output only exposes numeric sec:role-id values - not useful to a human until something resolves them. Both samples here call xdmp:role-name() / xdmp.roleName() on each role-id before returning, so the result reads as roles and capabilities instead of opaque integers. xdmp:document-get-permissions and xdmp.documentGetPermissions return the same four permissions in the same order, but in different shapes — XQuery works with sec:permission XML elements, JavaScript works with plain {capability, roleId} objects. Neither is wrong; pick whichever matches how the rest of your code already handles permissions.

Resolving a role ID to a role name is as far as a single document's permissions can take you. Permissions record which roles can act on a document — they do not record which users hold those roles. MarkLogic does not expose a single function that reverses a permission's role ID into a list of users; the Security database stores role membership separately from document permissions, and answering "which users have this role" means querying the Security database directly (for example, checking sec:user-get-roles() per user), not something this pack's document-scoped functions can do on their own.

For auditing at scale — "which documents grant the cleverllamas-llamaverse-writer role node-update access?" instead of "what does this one document grant?" — the next section runs that query directly against the same permission this pack already inspected above.

cts:document-permission-query

xdmp:document-get-permissions() answers "what can act on this one document?" one URI at a time. cts:document-permission-query() flips the question around: "which documents does this role and capability combination match?" - answered at index speed, across the whole database, without opening a single document fragment.

Source docs: https://docs.marklogic.com/11.0/cts:document-permission-query

Option / ArgumentWhat it controlsUsed here
$role-nameRole name(s) the permission must grantcleverllamas-llamaverse-writer - the same role seen in the xdmp:document-get-permissions() output above
$capabilityCapability the role must holdnode-update - one of the four capabilities returned above
{
  "id": "b3591799-54a8-44ea-87a1-d1c05659f781",
  "name": "Dana",
  "heightCm": 161,
  "weightKg": 144,
  "eyeColor": "Hazel",
  "hairColor": "Blonde",
  "breed": "Hybrid",
  "placeOfBirth": "Peterchester, Falkland Islands (Malvinas)",
  "description": "Dana is a hazel-eyed llama with blonde hair, standing 161 cm tall. Originally from Peterchester, Falkland Islands (Malvinas), Dana enjoys gardening, painting, hiking. Known for their calm and gentle personality, Dana is a beloved member of the llama community.",
  "relatedTo": {
    "id": "2233b726-9102-4e33-904e-d1fc5a805613",
    "relationship": "15th cousin 9th removed"
  },
  "classificationSpeciesKey": "Lama_glama_vellifer"
}
xquery version "1.0-ml";

(: Same wild llama used throughout this pack - the one with a "relatedTo"
 : pedigree entry - used here to confirm it is one of the documents this
 : query matches. :)
let $sample-doc := cts:search(
  collection("wild-llamas"),
  cts:json-property-scope-query("relatedTo", cts:true-query())
)[1]
let $q := cts:and-query((
  cts:collection-query("wild-llamas"),
  cts:document-permission-query("cleverllamas-llamaverse-writer", "node-update")
))
return
  (
    (: Index-only match count - no document fragments are opened to answer
     : this. :)
    "matching documents: " || cts:estimate($q),
    (: Confirms the same sample document used throughout this pack is one of
     : the matches, tying this scale query back to the single-document
     : xdmp:document-get-permissions() output above. :)
    "sample document matches: " || cts:contains($sample-doc, $q)
  )
'use strict';

// Same wild llama used throughout this pack - the one with a "relatedTo"
// pedigree entry - used here to confirm it is one of the documents this
// query matches.
const sampleDoc = cts.search(cts.andQuery([
  cts.collectionQuery('wild-llamas'),
  cts.jsonPropertyScopeQuery('relatedTo', cts.trueQuery())
])).toArray()[0];

const q = cts.andQuery([
  cts.collectionQuery('wild-llamas'),
  cts.documentPermissionQuery('cleverllamas-llamaverse-writer', 'node-update')
]);

Sequence.from([
  // Index-only match count - no document fragments are opened to answer
  // this.
  `matching documents: ${cts.estimate(q)}`,
  // Confirms the same sample document used throughout this pack is one of
  // the matches, tying this scale query back to the single-document
  // xdmp.documentGetPermissions() output above.
  `sample document matches: ${cts.contains(sampleDoc, q)}`
]);
matching documents: 4561
sample document matches: true

What to notice: this is a cts:query, so it combines with cts:and-query(), cts:collection-query(), and every other cts:query constructor exactly like a search filter - because that's what it is. cts:estimate() answers "how many" using only the universal index, and cts:contains() confirms the same wild-llama document used throughout this pack (the one whose individual permissions were just inspected above) is one of the 4,561 documents this database-wide query matches. For the deeper coverage of this function - including role-id-to-name resolution patterns and additional capability combinations - see Document Permissions Query.

xdmp:document-properties

A document's properties fragment is metadata storage that lives beside the content, not inside it

  • and it's usually empty until something puts a property there.

Source docs: https://docs.marklogic.com/11.0/xdmp:document-properties

Option / ArgumentWhat it controlsUsed here
$uriDocument URI whose properties fragment(s) should be returnedSample wild-llama document URI
{
  "id": "b3591799-54a8-44ea-87a1-d1c05659f781",
  "name": "Dana",
  "heightCm": 161,
  "weightKg": 144,
  "eyeColor": "Hazel",
  "hairColor": "Blonde",
  "breed": "Hybrid",
  "placeOfBirth": "Peterchester, Falkland Islands (Malvinas)",
  "description": "Dana is a hazel-eyed llama with blonde hair, standing 161 cm tall. Originally from Peterchester, Falkland Islands (Malvinas), Dana enjoys gardening, painting, hiking. Known for their calm and gentle personality, Dana is a beloved member of the llama community.",
  "relatedTo": {
    "id": "2233b726-9102-4e33-904e-d1fc5a805613",
    "relationship": "15th cousin 9th removed"
  },
  "classificationSpeciesKey": "Lama_glama_vellifer"
}
xquery version "1.0-ml";

(: Same wild llama used throughout this pack - the one with a "relatedTo"
 : pedigree entry. :)
let $sample-doc := cts:search(
  collection("wild-llamas"),
  cts:json-property-scope-query("relatedTo", cts:true-query())
)[1]
let $sample-uri := xdmp:node-uri($sample-doc)
return
  (
    $sample-uri,
    (: The properties fragment is a separate document, stored alongside the
     : content but addressed by the same URI. This is the one write in this
     : pack - it exists to give xdmp:document-properties() something to
     : return, since a fresh llamaverse document has no properties fragment
     : until something creates one. :)
    xdmp:document-set-property($sample-uri,
      <inspection-note>Reviewed via document-inspection-starter-pack</inspection-note>),
    xdmp:document-properties($sample-uri)
  )
'use strict';
declareUpdate();

// Same wild llama used throughout this pack - the one with a "relatedTo"
// pedigree entry.
const sampleDoc = cts.search(cts.andQuery([
  cts.collectionQuery('wild-llamas'),
  cts.jsonPropertyScopeQuery('relatedTo', cts.trueQuery())
])).toArray()[0];

if (!sampleDoc) {
  throw new Error("NO-LLAMAVERSE: No wild-llamas document with a 'relatedTo' entry was found.");
}

const sampleUri = xdmp.nodeUri(sampleDoc);

// The properties fragment is a separate document, stored alongside the
// content but addressed by the same URI. This is the one write in this pack
// - it exists to give xdmp.documentProperties() something to return, since a
// fresh llamaverse document has no properties fragment until something
// creates one.
const noteElement = fn.head(
  xdmp.unquote('<inspection-note>Reviewed via document-inspection-starter-pack</inspection-note>')
).root;
xdmp.documentSetProperty(sampleUri, noteElement);

Sequence.from([sampleUri, ...xdmp.documentProperties(sampleUri).toArray()]);
/cleverllamas/llamaverse/raw/wild-llamas/llamas/b3591799-54a8-44ea-87a1-d1c05659f781.json
<prop:properties xmlns:prop="http://marklogic.com/xdmp/property">
  <inspection-note>Reviewed via document-inspection-starter-pack</inspection-note>
</prop:properties>
LineValue
1/cleverllamas/llamaverse/raw/wild-llamas/llamas/b3591799-54a8-44ea-87a1-d1c05659f781.json
2prop:properties fragment containing one inspection-note element

What to notice: this is the first of three writes in this pack - the other two set document metadata, a separate storage mechanism covered next. A fresh llamaverse document has no properties fragment at all - xdmp:document-properties() simply returns an empty sequence until something calls xdmp:document-set-property() (or xdmp:document-add-properties()) to create one. The sample above sets an inspection-note property first so there's something to read back. The properties fragment has its own root (prop:properties, in the http://marklogic.com/xdmp/property namespace) - it is a distinct document from the content fragment, addressed by the same URI but never returned by fn:doc() or xdmp:node-collections() on the content itself. There is no xdmp:node-properties() - properties are document-scoped only, unlike collections and permissions, which exist at both the document and node level.

xdmp:document-get-metadata and xdmp:document-set-metadata

Metadata is a second, separate storage mechanism sitting beside a document's content - a plain JSON key-value map, not an XML fragment like properties. It's easy to conflate the two because both are "extra facts stored alongside the document," but they have different shapes, different functions, and (as this section shows) different write semantics.

Source docs: https://docs.marklogic.com/11.0/xdmp:document-get-metadata, https://docs.marklogic.com/11.0/xdmp:document-set-metadata

Option / ArgumentWhat it controlsUsed here
$uriDocument URI whose metadata should be read or writtenSample wild-llama document URI
$metadataMap of key-value pairs to store as the document's metadatainspectedBy and inspectionPass keys
$key-nameSingle metadata key to read with xdmp:document-get-metadata-value()inspectedBy
{
  "id": "b3591799-54a8-44ea-87a1-d1c05659f781",
  "name": "Dana",
  "heightCm": 161,
  "weightKg": 144,
  "eyeColor": "Hazel",
  "hairColor": "Blonde",
  "breed": "Hybrid",
  "placeOfBirth": "Peterchester, Falkland Islands (Malvinas)",
  "description": "Dana is a hazel-eyed llama with blonde hair, standing 161 cm tall. Originally from Peterchester, Falkland Islands (Malvinas), Dana enjoys gardening, painting, hiking. Known for their calm and gentle personality, Dana is a beloved member of the llama community.",
  "relatedTo": {
    "id": "2233b726-9102-4e33-904e-d1fc5a805613",
    "relationship": "15th cousin 9th removed"
  },
  "classificationSpeciesKey": "Lama_glama_vellifer"
}
xquery version "1.0-ml";

(: Same wild llama used throughout this pack - the one with a "relatedTo"
 : pedigree entry. :)
let $sample-doc := cts:search(
  collection("wild-llamas"),
  cts:json-property-scope-query("relatedTo", cts:true-query())
)[1]
let $sample-uri := xdmp:node-uri($sample-doc)
return
  (
    $sample-uri,
    (: Metadata is a JSON key-value map stored beside the document - distinct
     : from the prop:properties XML fragment. xdmp:document-set-metadata()
     : REPLACES all existing metadata on the document, it does not merge with
     : whatever was there before. A fresh llamaverse document has no metadata
     : until something sets it. :)
    xdmp:document-set-metadata($sample-uri,
      map:map()
        => map:with("inspectedBy", "document-inspection-starter-pack")
        => map:with("inspectionPass", "2")),
    xdmp:document-get-metadata($sample-uri),
    xdmp:document-get-metadata-value($sample-uri, "inspectedBy")
  )
'use strict';
declareUpdate();

// Same wild llama used throughout this pack - the one with a "relatedTo"
// pedigree entry.
const sampleDoc = cts.search(cts.andQuery([
  cts.collectionQuery('wild-llamas'),
  cts.jsonPropertyScopeQuery('relatedTo', cts.trueQuery())
])).toArray()[0];

if (!sampleDoc) {
  throw new Error("NO-LLAMAVERSE: No wild-llamas document with a 'relatedTo' entry was found.");
}

const sampleUri = xdmp.nodeUri(sampleDoc);

// Metadata is a JSON key-value map stored beside the document - distinct
// from the prop:properties XML fragment. xdmp.documentSetMetadata()
// REPLACES all existing metadata on the document, it does not merge with
// whatever was there before. A fresh llamaverse document has no metadata
// until something sets it.
xdmp.documentSetMetadata(sampleUri, {
  inspectedBy: 'document-inspection-starter-pack',
  inspectionPass: '2'
});

Sequence.from([
  sampleUri,
  xdmp.documentGetMetadata(sampleUri),
  xdmp.documentGetMetadataValue(sampleUri, 'inspectedBy')
]);
/cleverllamas/llamaverse/raw/wild-llamas/llamas/b3591799-54a8-44ea-87a1-d1c05659f781.json
{"inspectedBy":"document-inspection-starter-pack", "inspectionPass":"2"}
document-inspection-starter-pack
LineValue
1/cleverllamas/llamaverse/raw/wild-llamas/llamas/b3591799-54a8-44ea-87a1-d1c05659f781.json
2Metadata map: inspectedBy = document-inspection-starter-pack, inspectionPass = 2
3xdmp:document-get-metadata-value() for inspectedBy alone

What to notice: like properties, a fresh llamaverse document has no metadata at all - xdmp:document-get-metadata() returns an empty map until something calls xdmp:document-set-metadata(). Unlike xdmp:document-set-property(), which adds to whatever properties already exist, xdmp:document-set-metadata() replaces the entire metadata map every time it's called - there's no additive equivalent for a single key, and no xdmp:document-set-metadata-value() function to add one key without touching the rest. If you need to add a key without losing existing metadata, read the current map first, merge in your change, and write the whole map back. xdmp.documentGetMetadata() in JavaScript returns the same map, and both languages serialize it identically over /v1/eval regardless of which language set it.

xdmp:node-metadata and xdmp:node-metadata-value

Metadata has a node-level reader too, and it behaves exactly like the other xdmp:node-xxx functions in this pack: call it on any node, however deep, and it resolves back to the owning document's metadata - no URI lookup required.

Source docs: https://docs.marklogic.com/11.0/xdmp:node-metadata, https://docs.marklogic.com/11.0/xdmp:node-metadata-value

Option / ArgumentWhat it controlsUsed here
$nodeNode whose owning document's metadata should be returnedThe relationship text node, three levels inside the sample document
$key-nameSingle metadata key to read with xdmp:node-metadata-value()inspectedBy
{
  "id": "b3591799-54a8-44ea-87a1-d1c05659f781",
  "name": "Dana",
  "heightCm": 161,
  "weightKg": 144,
  "eyeColor": "Hazel",
  "hairColor": "Blonde",
  "breed": "Hybrid",
  "placeOfBirth": "Peterchester, Falkland Islands (Malvinas)",
  "description": "Dana is a hazel-eyed llama with blonde hair, standing 161 cm tall. Originally from Peterchester, Falkland Islands (Malvinas), Dana enjoys gardening, painting, hiking. Known for their calm and gentle personality, Dana is a beloved member of the llama community.",
  "relatedTo": {
    "id": "2233b726-9102-4e33-904e-d1fc5a805613",
    "relationship": "15th cousin 9th removed"
  },
  "classificationSpeciesKey": "Lama_glama_vellifer"
}
xquery version "1.0-ml";

(: Same wild llama used throughout this pack - the one with a "relatedTo"
 : pedigree entry. This reaches three levels deep, past the document node,
 : past the relatedTo object, down to the relationship text node. :)
let $sample-doc := cts:search(
  collection("wild-llamas"),
  cts:json-property-scope-query("relatedTo", cts:true-query())
)[1]
let $deep-node := $sample-doc/relatedTo/relationship
let $sample-uri := xdmp:node-uri($deep-node)
return
  (
    (: There is no node-level metadata setter - metadata is written through
     : the document URI, just like properties. This resets metadata to the
     : same values the xdmp:document-get-metadata example sets, so this
     : sample stays runnable on its own. :)
    xdmp:document-set-metadata($sample-uri,
      map:map()
        => map:with("inspectedBy", "document-inspection-starter-pack")
        => map:with("inspectionPass", "2")),
    (: ...but reading metadata back works directly from the deep node, no URI
     : lookup required - the same pattern as node-uri, node-kind,
     : node-collections, and node-permissions above. :)
    xdmp:node-metadata($deep-node),
    xdmp:node-metadata-value($deep-node, "inspectedBy")
  )
'use strict';
declareUpdate();

// Same wild llama used throughout this pack - the one with a "relatedTo"
// pedigree entry. This reaches three levels deep, past the document node,
// past the relatedTo object, down to the relationship text node.
const sampleDoc = cts.search(cts.andQuery([
  cts.collectionQuery('wild-llamas'),
  cts.jsonPropertyScopeQuery('relatedTo', cts.trueQuery())
])).toArray()[0];

if (!sampleDoc) {
  throw new Error("NO-LLAMAVERSE: No wild-llamas document with a 'relatedTo' entry was found.");
}

const deepNode = fn.head(sampleDoc.xpath('/relatedTo/relationship'));
const sampleUri = xdmp.nodeUri(deepNode);

// There is no node-level metadata setter - metadata is written through the
// document URI, just like properties. This resets metadata to the same
// values the xdmp.documentGetMetadata example sets, so this sample stays
// runnable on its own.
xdmp.documentSetMetadata(sampleUri, {
  inspectedBy: 'document-inspection-starter-pack',
  inspectionPass: '2'
});

// ...but reading metadata back works directly from the deep node, no URI
// lookup required - the same pattern as nodeUri, nodeKind, nodeCollections,
// and nodePermissions above.
Sequence.from([
  xdmp.nodeMetadata(deepNode),
  xdmp.nodeMetadataValue(deepNode, 'inspectedBy')
]);
{"inspectedBy":"document-inspection-starter-pack", "inspectionPass":"2"}
document-inspection-starter-pack

What to notice: there is no node-level metadata setter - xdmp:document-set-metadata() is the only way to write metadata, and it always takes a URI. But reading it back with xdmp:node-metadata() and xdmp:node-metadata-value() works directly from the same deep relationship text node used by xdmp:node-uri(), xdmp:node-kind(), xdmp:node-collections(), and xdmp:node-permissions() earlier in this pack - confirming metadata joins collections and permissions as a fact any node can answer about its own document, even though writing it stays document-scoped like properties.

Documents Tell Stories

Decision rule: when access behaviour surprises you, treat document metadata as source of truth before changing security code. And when you're not sure whether to reach for xdmp:document-xxx or xdmp:node-xxx, ask whether you already have the node in hand — if you do, skip the URI lookup and use the node function directly.

Once you understand document metadata, explore how it connects to the broader system:

Need Some Help?


Looking for more information on this subject or any other topic related to MarkLogic? Contact Us (info@cleverllamas.com) to find out how we can assist you with consulting or training!