Optic Data Manipulation Starter Pack

Shape XML, run expression libraries at runtime, and execute update plans cleanly

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

The source shape, the transformation logic, and the output stay together so the before and after are never a mystery.

This pack covers the Optic manipulation surface you reach for once a source is not enough on its own. That means turning XML into rows, using expression helper libraries inside plans, and understanding when a plan stops being read-only.

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.

Language and Scope

These examples use the same sample-document structure as the cts, xdmp, and other Optic starter packs.

One naming detail matters here: the documented Optic update function is op:execute(), not op:exec().

Most sections in this pack are read-only. The final op:execute() section is update-oriented and should be treated as a content-changing pattern.

The code assets in this pack were validated against the local MarkLogic 11 runtime used by this repository via tooling/marklogic/validate-optic-starter-packs.sh.

API Context for This Pack

Context ItemWhat this pack uses
Primary sourcesXML seed documents, literal row inputs, and update payload rows
Primary functionsop:xpath, op:select, op:execute, op:write, op:join-cross-product
Expression helpersofn, oxs, and oxdmp helper libraries in Optic expressions
Output styleSmall row sets for read examples; explicit mutation note for update examples

Expression Libraries in Optic Plans

The Optic API is not limited to op:* functions. You can also import Optic expression helper libraries that wrap familiar function families for use inside plans, and those expressions are evaluated when the plan executes.

In XQuery you import these helpers explicitly (ofn, oxs, oxdmp). In JavaScript the same families are available as op.fn, op.xs, and op.xdmp.

This example proves runtime evaluation directly:

  1. Capture a timestamp outside the plan.
  2. Sleep for five seconds.
  3. Run a plan that computes ofn:current-dateTime(), oxs:dateTime(...), and oxdmp:random(...) inside Optic expressions.

If those expressions run at plan execution time, the runtime values must be later than the captured value.

The core libraries used here are:

LibraryPurposeTypical use in a plan
ofnOptic wrapper for W3C fn functionsRuntime timestamp and string conversion inside the plan
oxsOptic wrapper for xs constructorsTyped xs:dateTime construction inside a plan expression
oxdmpOptic wrapper for useful xdmp functionsRuntime random value generation within Optic
Option / ArgumentWhat it controlsUsed here
ofn:current-dateTime()Computes runtime timestamp during plan executionShows runtime value after a 5-second sleep
oxs:dateTime(...)Constructs a typed dateTime expression in-planShows explicit type construction in the plan
oxdmp:random(...)Computes server-side random value inside plan expressionShows execution-time helper behaviour
{
  "demoId": "runtime-proof",
  "capturedAt": "set before sleep()",
  "runtimeAt": "computed inside the plan"
}
xquery version "1.0-ml";
import module namespace op = "http://marklogic.com/optic" at "/MarkLogic/optic.xqy";
import module namespace ofn = "http://marklogic.com/optic/expression/fn" at "/MarkLogic/optic/optic-fn.xqy";
import module namespace oxs = "http://marklogic.com/optic/expression/xs" at "/MarkLogic/optic/optic-xs.xqy";
import module namespace oxdmp = "http://marklogic.com/optic/expression/xdmp" at "/MarkLogic/optic/optic-xdmp.xqy";

let $captured-at := fn:current-dateTime()
let $_ := xdmp:sleep(5000)
let $captured := op:from-literals((
  map:entry("demoId", "runtime-proof")
    => map:with("capturedAt", $captured-at)
))
let $runtime := op:from-literals((
  map:entry("runtimeId", "runtime-proof")
))

return
$captured
  => op:join-cross-product(
       $runtime,
       op:eq(op:col("demoId"), op:col("runtimeId"))
     )
  => op:select((
       "demoId",
       "capturedAt",
       op:as("runtimeAt", ofn:current-dateTime()),
       op:as("runtimeAsDateTime", oxs:dateTime(ofn:string(ofn:current-dateTime()))),
       op:as("runtimeAfterCaptured", op:gt(ofn:current-dateTime(), op:col("capturedAt"))),
       op:as("runtimeRandom", oxdmp:random(1000000))
     ), "clockCheck")
  => op:result()
'use strict';

const op = require('/MarkLogic/optic');

const capturedAt = fn.currentDateTime();
xdmp.sleep(5000);

const captured = op.fromLiterals([
  {
    demoId: 'runtime-proof',
    capturedAt
  }
]);

const runtime = op.fromLiterals([
  {
    runtimeId: 'runtime-proof'
  }
]);

const results = captured
  .joinCrossProduct(runtime, op.eq(op.col('demoId'), op.col('runtimeId')))
  .select([
    'demoId',
    'capturedAt',
    op.as('runtimeAt', op.fn.currentDateTime()),
    op.as('runtimeAsDateTime', op.xs.dateTime(op.fn.string(op.fn.currentDateTime()))),
    op.as('runtimeAfterCaptured', op.gt(op.fn.currentDateTime(), op.col('capturedAt'))),
    op.as('runtimeRandom', op.xdmp.random(1000000))
  ], 'clockCheck')
  .result();

({
  sample: 'optic/data-manipulation-starter-pack/assets/expression-libraries.sjs',
  kind: Array.isArray(results) ? (results.every((item) => typeof item === 'object' && 'subject' in item && 'predicate' in item && 'object' in item) ? 'triples' : 'rows') : ((results !== null && typeof results === 'object') ? 'object' : 'scalar'),
  count: Array.isArray(results) ? results.length : 0,
  data: results
});
{"clockCheck.demoId":"runtime-proof", "clockCheck.capturedAt":"2026-07-23T12:23:50.163682Z", "clockCheck.runtimeAt":"2026-07-23T12:23:55.524298Z", "clockCheck.runtimeAsDateTime":"2026-07-23T12:23:55.524298Z", "clockCheck.runtimeAfterCaptured":true, "clockCheck.runtimeRandom":328961}
clockCheck.demoIdclockCheck.capturedAtclockCheck.runtimeAtclockCheck.runtimeAsDateTimeclockCheck.runtimeAfterCapturedclockCheck.runtimeRandom
runtime-proof2026-07-23T12:23:50.163682Z2026-07-23T12:23:55.524298Z2026-07-23T12:23:55.524298Ztrue328961

What to notice: runtimeAfterCaptured is true after a deliberate 5-second pause, which confirms these expression-library calls execute at runtime inside the plan, not just when the script is assembled.

op:xpath()

op:xpath() is the bridge from XML structure into Optic rows. Use it when the source is hierarchical but the next question you need to answer is tabular.

This is the practical answer to the earlier shorthand op:from-xpath: the source is usually op:from-doc-descriptors() or another document-bearing plan, and op:xpath() is the operation that projects XML into columns.

Option / ArgumentWhat it controlsUsed here
columnOutput column name for extracted valuesForest status projection columns
pathXPath expression evaluated against each source documentXPath paths for ID, name, and state
<all-forests>
  <forest-status>
    <forest-id>1</forest-id>
    <forest-name>Documents-1</forest-name>
    <state>online</state>
  </forest-status>
</all-forests>
xquery version "1.0-ml";
import module namespace op = "http://marklogic.com/optic" at "/MarkLogic/optic.xqy";
import module namespace ofn = "http://marklogic.com/optic/expression/fn" at "/MarkLogic/optic/optic-fn.xqy";

let $forestDoc := document {
  element all-forests {
    for $forest in subsequence(xdmp:forest-status(xdmp:forests()), 1, 3)
    return $forest
  }
}

let $descriptors :=
  for $forest at $position in $forestDoc/*:all-forests/*:forest-status
  return
    map:entry("uri", "/cleverllamas/llamaverse/scratch/reports/forest-status-" || $position || ".xml")
      => map:with("doc", document { $forest })

return
op:from-doc-descriptors($descriptors)
  => op:select((
       "uri",
       op:as("forestName", ofn:string(op:xpath("doc", "/*:forest-status/*:forest-name"))),
       op:as("availability", ofn:string(op:xpath("doc", "/*:forest-status/*:availability"))),
       op:as("state", ofn:string(op:xpath("doc", "/*:forest-status/*:state")))
     ))
  => op:result()
'use strict';

const op = require('/MarkLogic/optic');

const descriptors = xdmp.forests().toArray().map((forestId) =>
  {
    const status = JSON.parse(xdmp.quote(xdmp.forestStatus(forestId)));
    return {
      uri: `/forest-status/${forestId}.xml`,
      doc: xdmp.unquote(
        `<status>
          <forest-id>${status.forestId}</forest-id>
          <forest-name>${status.forestName}</forest-name>
          <state>${status.state}</state>
          <availability>${status.availability}</availability>
        </status>`
      )
    };
  }
);

const results = op.fromDocDescriptors(descriptors)
  .select([
    op.as('forestId', op.fn.string(op.xpath('doc', '/status/forest-id'))),
    op.as('forestName', op.fn.string(op.xpath('doc', '/status/forest-name')))
  ])
  .result();

({
  sample: 'optic/data-manipulation-starter-pack/assets/op-xpath-forest-status.sjs',
  kind: Array.isArray(results) ? (results.every((item) => typeof item === 'object' && 'subject' in item && 'predicate' in item && 'object' in item) ? 'triples' : 'rows') : ((results !== null && typeof results === 'object') ? 'object' : 'scalar'),
  count: Array.isArray(results) ? results.length : 0,
  data: results
});
{"uri":"/cleverllamas/llamaverse/scratch/reports/forest-status-1.xml", "forestName":"Last-Login", "availability":"online", "state":"open"}
{"uri":"/cleverllamas/llamaverse/scratch/reports/forest-status-2.xml", "forestName":"cleverllamas-content-1", "availability":"online", "state":"open"}
{"uri":"/cleverllamas/llamaverse/scratch/reports/forest-status-3.xml", "forestName":"Schemas", "availability":"online", "state":"open"}
uriforestNameavailabilitystate
/cleverllamas/llamaverse/scratch/reports/forest-status-1.xmlLast-Loginonlineopen
/cleverllamas/llamaverse/scratch/reports/forest-status-2.xmlcleverllamas-content-1onlineopen
/cleverllamas/llamaverse/scratch/reports/forest-status-3.xmlSchemasonlineopen

What to notice: the interesting transformation is not the descriptor source by itself. It is the moment op:xpath() turns the seeded XML documents into stable report columns.

op:xpath() sequence expansion with op:unnest-inner()

Yes, this pattern is covered now explicitly: op:xpath() can extract a multi-value sequence (for example, repeated <skill> nodes), and op:unnest-inner() expands that sequence into one row per value.

Source docs: https://docs.marklogic.com/op:xpathhttps://docs.marklogic.com/op:unnest-inner

Option / ArgumentWhat it controlsUsed here
op:xpath("doc", "/llama/skills/skill/text()")Extracts repeated skill values as a sequenceCreates skillSeq from XML nodes
op:unnest-inner(input, value, ordinal)Flattens sequence into rows and keeps positionProduces skill + skillOrdinality rows
<llama>
  <name>Cameron</name>
  <skills>
    <skill>xpath</skill>
    <skill>security</skill>
    <skill>optic</skill>
  </skills>
</llama>
xquery version "1.0-ml";
import module namespace op = "http://marklogic.com/optic" at "/MarkLogic/optic.xqy";
import module namespace ofn = "http://marklogic.com/optic/expression/fn" at "/MarkLogic/optic/optic-fn.xqy";

let $descriptors := (
  map:entry("uri", "/llama/skills/aaron.xml")
    => map:with("doc", xdmp:unquote('<llama><name>Aaron</name><skills><skill>xpath</skill><skill>optic</skill></skills></llama>')),
  map:entry("uri", "/llama/skills/cameron.xml")
    => map:with("doc", xdmp:unquote('<llama><name>Cameron</name><skills><skill>xpath</skill><skill>security</skill><skill>optic</skill></skills></llama>'))
)

return
op:from-doc-descriptors($descriptors)
  => op:select((
       "uri",
       op:as("llamaName", ofn:string(op:xpath("doc", "/llama/name"))),
       op:as("skillSeq", op:xpath("doc", "/llama/skills/skill/text()"))
     ))
  => op:unnest-inner("skillSeq", "skill", "skillOrdinality")
  => op:order-by((op:asc("llamaName"), op:asc("skillOrdinality")))
  => op:result()
'use strict';

const op = require('/MarkLogic/optic');

const descriptors = [
  {
    uri: '/llama/skills/aaron.xml',
    doc: xdmp.unquote('<llama><name>Aaron</name><skills><skill>xpath</skill><skill>optic</skill></skills></llama>')
  },
  {
    uri: '/llama/skills/cameron.xml',
    doc: xdmp.unquote('<llama><name>Cameron</name><skills><skill>xpath</skill><skill>security</skill><skill>optic</skill></skills></llama>')
  }
];

const results = op.fromDocDescriptors(descriptors)
  .select([
    'uri',
    op.as('llamaName', op.fn.string(op.xpath('doc', '/llama/name'))),
    op.as('skillSeq', op.xpath('doc', '/llama/skills/skill/text()'))
  ])
  .unnestInner('skillSeq', 'skill', 'skillOrdinality')
  .orderBy([op.asc('llamaName'), op.asc('skillOrdinality')])
  .result();

({
  sample: 'optic/data-manipulation-starter-pack/assets/op-xpath-sequence-expand.sjs',
  kind: Array.isArray(results) ? (results.every((item) => typeof item === 'object' && 'subject' in item && 'predicate' in item && 'object' in item) ? 'triples' : 'rows') : ((results !== null && typeof results === 'object') ? 'object' : 'scalar'),
  count: Array.isArray(results) ? results.length : 0,
  data: results
});
{"uri":"/llama/skills/aaron.xml", "llamaName":"Aaron", "skill":"xpath", "skillSeq":["xpath", "optic"], "skillOrdinality":1}
{"uri":"/llama/skills/aaron.xml", "llamaName":"Aaron", "skill":"optic", "skillSeq":["xpath", "optic"], "skillOrdinality":2}
{"uri":"/llama/skills/cameron.xml", "llamaName":"Cameron", "skill":"xpath", "skillSeq":["xpath", "security", "optic"], "skillOrdinality":1}
{"uri":"/llama/skills/cameron.xml", "llamaName":"Cameron", "skill":"security", "skillSeq":["xpath", "security", "optic"], "skillOrdinality":2}
{"uri":"/llama/skills/cameron.xml", "llamaName":"Cameron", "skill":"optic", "skillSeq":["xpath", "security", "optic"], "skillOrdinality":3}
urillamaNameskillskillSeqskillOrdinality
/llama/skills/aaron.xmlAaronxpathxpath,optic1
/llama/skills/aaron.xmlAaronopticxpath,optic2
/llama/skills/cameron.xmlCameronxpathxpath,security,optic1
/llama/skills/cameron.xmlCameronsecurityxpath,security,optic2
/llama/skills/cameron.xmlCameronopticxpath,security,optic3

What to notice: this is the concrete "XML into rows" transition for repeating nodes. op:xpath() extracts the sequence, and op:unnest-inner() gives you row-level values that can be joined, filtered, grouped, or written.

op:select()

op:select() is the projection and shaping stage for Optic rows. It decides exactly which columns leave the plan step, what they are called, and whether they are raw values or derived expressions.

In practice, this is where your output contract becomes explicit.

Source docs: https://docs.marklogic.com/op:select

Option / ArgumentWhat it controlsUsed here
columnsWhich output columns are projected and how each is expressedMix of pass-through fields and derived labels/flags
qualifierOutput qualifier for projected columnsshape qualifier for readable result structure
{
  "name": "Aaron",
  "breed": "Huacaya",
  "heightCm": 172,
  "placeOfBirth": "Cusco"
}
xquery version "1.0-ml";
import module namespace op = "http://marklogic.com/optic" at "/MarkLogic/optic.xqy";
import module namespace ofn = "http://marklogic.com/optic/expression/fn" at "/MarkLogic/optic/optic-fn.xqy";

op:from-literals((
  map:entry("name", "Aaron")
    => map:with("breed", "Huacaya")
    => map:with("heightCm", 172)
    => map:with("placeOfBirth", "Cusco"),
  map:entry("name", "Cameron")
    => map:with("breed", "Suri")
    => map:with("heightCm", 167)
    => map:with("placeOfBirth", "Quito"),
  map:entry("name", "Richard")
    => map:with("breed", "Huacaya")
    => map:with("heightCm", 176)
    => map:with("placeOfBirth", "Arequipa")
))
  => op:select((
       op:as("llamaName", op:col("name")),
       op:as("profileLabel", ofn:concat((op:col("name"), " (", op:col("breed"), ") from ", op:col("placeOfBirth")))),
       op:as("heightCm", op:col("heightCm")),
       op:as("isTall", op:gt(op:col("heightCm"), 170))
     ), "shape")
  => op:order-by(op:asc("llamaName"))
  => op:result()
'use strict';

const op = require('/MarkLogic/optic');

const results = op.fromLiterals([
  { name: 'Aaron', breed: 'Huacaya', heightCm: 172, placeOfBirth: 'Cusco' },
  { name: 'Cameron', breed: 'Suri', heightCm: 167, placeOfBirth: 'Quito' },
  { name: 'Richard', breed: 'Huacaya', heightCm: 176, placeOfBirth: 'Arequipa' }
])
  .select([
    op.as('llamaName', op.col('name')),
    op.as('profileLabel', op.fn.concat(op.col('name'), ' (', op.col('breed'), ') from ', op.col('placeOfBirth'))),
    op.as('heightCm', op.col('heightCm')),
    op.as('isTall', op.gt(op.col('heightCm'), 170))
  ], 'shape')
  .orderBy(op.asc('llamaName'))
  .result();

({
  sample: 'optic/data-manipulation-starter-pack/assets/op-select-shaping.sjs',
  kind: Array.isArray(results) ? (results.every((item) => typeof item === 'object' && 'subject' in item && 'predicate' in item && 'object' in item) ? 'triples' : 'rows') : ((results !== null && typeof results === 'object') ? 'object' : 'scalar'),
  count: Array.isArray(results) ? results.length : 0,
  data: results
});
{"shape.llamaName":"Aaron", "shape.profileLabel":"Aaron (Huacaya) from Cusco", "shape.heightCm":172, "shape.isTall":true}
{"shape.llamaName":"Cameron", "shape.profileLabel":"Cameron (Suri) from Quito", "shape.heightCm":167, "shape.isTall":false}
{"shape.llamaName":"Richard", "shape.profileLabel":"Richard (Huacaya) from Arequipa", "shape.heightCm":176, "shape.isTall":true}
shape.llamaNameshape.profileLabelshape.heightCmshape.isTall
AaronAaron (Huacaya) from Cusco172true
CameronCameron (Suri) from Quito167false
RichardRichard (Huacaya) from Arequipa176true

What to notice: op:select() is where result semantics become deliberate. The input rows carry several fields, but the selected output becomes a concise contract with clear names (llamaName, profileLabel) and computed meaning (isTall).

Privilege Snapshot Pipeline with op:execute()

Once a plan becomes update-oriented, the control point changes. Instead of asking for rows back, you often want to generate write rows, call op:write(), and then finish with op:execute().

This example uses a real admin-side use case: snapshot all privileges for cleverllamas-mother, resolve each privilege ID to its real name, and persist one document per privilege through an Optic update plan.

The flattening step uses op:unnest-inner(): a single row containing a privilege sequence becomes one row per privilege with an ordinal column. Those unnested rows then feed the write pipeline.

Option / ArgumentWhat it controlsUsed here
xdmp:user-privileges()Returns privilege IDs for the target userSource list for row expansion
op:unnest-inner()Flattens sequence/array values into one row per valueTurns privilege sequence into write-ready rows with ordinality
xdmp:privilege-name()Resolves privilege ID to real privilege namePersists meaningful privilege details
op:write(...)Defines write operation for the bound payloadDocument insert/update operation
op:execute()Executes the update planRuns the write plan and commits changes
{
  "user": "cleverllamas-mother",
  "privilegeIds": ["..."],
  "writeRows": "one row per privilege"
}
xquery version "1.0-ml";
import module namespace op = "http://marklogic.com/optic" at "/MarkLogic/optic.xqy";
import module namespace ofn = "http://marklogic.com/optic/expression/fn" at "/MarkLogic/optic/optic-fn.xqy";
declare option xdmp:update "true";

let $user-name := "cleverllamas-mother"
let $privilege-csv := fn:string-join(
  for $privilege-id in xdmp:user-privileges($user-name)
  return fn:string($privilege-id),
  ","
)

let $privilege-rows :=
  op:from-literals((
    map:entry("userName", $user-name)
      => map:with("privilegeCsv", $privilege-csv)
  ))
    => op:bind((
         op:as("privilegeSeq", ofn:tokenize(op:col("privilegeCsv"), ","))
       ))
    => op:unnest-inner("privilegeSeq", "privilegeId", "ordinality")
    => op:select(("userName", "privilegeId", "ordinality"))
    => op:result()

let $rows :=
  for $privilege-row in $privilege-rows
  let $privilege-id := map:get($privilege-row, "privilegeId")
  let $ordinality := map:get($privilege-row, "ordinality")
  let $privilege-name := xdmp:privilege-name(xs:unsignedLong($privilege-id))
  let $uri := "/cleverllamas/llamaverse/scratch/optic-update/privileges/" || $user-name || "-" || fn:string($ordinality) || ".json"
  return
    map:entry("uri", $uri)
      => map:with("doc", object-node {
           "user": $user-name,
           "privilegeId": fn:string($privilege-id),
         "ordinality": $ordinality,
           "privilegeName": $privilege-name,
           "source": "op-execute-privilege-snapshot"
         })
      => map:with("collections", ("optic-execute-demo", "optic-privilege-snapshot"))

let $_ :=
  op:from-param("bindingParam", (), op:doc-col-types())
    => op:write()
    => op:execute(
         map:entry("bindingParam", $rows),
         ("trace=opticPrivilegeSnapshot", "optimize=1")
       )

return
  <result>
    <user>{ $user-name }</user>
    <written>{ count($rows) }</written>
    <samples>{
      for $row in subsequence($rows, 1, 4)
      return
        <sample>
          <uri>{ map:get($row, "uri") }</uri>
          <doc>{ xdmp:to-json-string(map:get($row, "doc")) }</doc>
        </sample>
    }</samples>
  </result>
'use strict';

declareUpdate();

const op = require('/MarkLogic/optic');

const userName = 'cleverllamas-mother';
const privilegeCsv = xdmp.userPrivileges(userName).toArray().map(String).join(',');

const privilegeRows = op.fromLiterals([
  {
    userName,
    privilegeCsv
  }
])
  .bind([
    op.as('privilegeSeq', op.fn.tokenize(op.col('privilegeCsv'), ','))
  ])
  .unnestInner('privilegeSeq', 'privilegeId', 'ordinality')
  .select(['userName', 'privilegeId', 'ordinality'])
  .result()
  .toArray();

const rows = privilegeRows.map((row) => ({
  uri: `/cleverllamas/llamaverse/scratch/optic-update/privileges/${userName}-${row.ordinality}.json`,
  doc: {
    user: userName,
    privilegeId: String(row.privilegeId),
    ordinality: row.ordinality,
    privilegeName: xdmp.privilegeName(row.privilegeId),
    source: 'op-execute-privilege-snapshot'
  },
  collections: ['optic-execute-demo', 'optic-privilege-snapshot']
}));

op.fromLiterals(rows).write().execute();

const results = {
  user: userName,
  written: rows.length,
  samples: rows.slice(0, 4)
};

({
  sample: 'optic/data-manipulation-starter-pack/assets/op-execute-from-param.sjs',
  kind: Array.isArray(results) ? (results.every((item) => typeof item === 'object' && 'subject' in item && 'predicate' in item && 'object' in item) ? 'triples' : 'rows') : ((results !== null && typeof results === 'object') ? 'object' : 'scalar'),
  count: Array.isArray(results) ? results.length : 0,
  data: results
});
X-Path: /result
<result><user>cleverllamas-mother</user><written>4</written><samples><sample><uri>/cleverllamas/llamaverse/scratch/optic-update/privileges/cleverllamas-mother-1.json</uri><doc>{"user":"cleverllamas-mother", "privilegeId":"6226129278634019154", "ordinality":1, "privilegeName":"xdmp:eval", "source":"op-execute-privilege-snapshot"}</doc></sample><sample><uri>/cleverllamas/llamaverse/scratch/optic-update/privileges/cleverllamas-mother-2.json</uri><doc>{"user":"cleverllamas-mother", "privilegeId":"2867400659384236357", "ordinality":2, "privilegeName":"cleverllamas-llamaverse", "source":"op-execute-privilege-snapshot"}</doc></sample><sample><uri>/cleverllamas/llamaverse/scratch/optic-update/privileges/cleverllamas-mother-3.json</uri><doc>{"user":"cleverllamas-mother", "privilegeId":"9230786184163395570", "ordinality":3, "privilegeName":"xdmp:eval-in", "source":"op-execute-privilege-snapshot"}</doc></sample><sample><uri>/cleverllamas/llamaverse/scratch/optic-update/privileges/cleverllamas-mother-4.json</uri><doc>{"user":"cleverllamas-mother", "privilegeId":"6866748295311171023", "ordinality":4, "privilegeName":"unprotected-collections", "source":"op-execute-privilege-snapshot"}</doc></sample></samples></result>
OrdinalityPrivilege nameWritten URI
1xdmp:eval/cleverllamas/llamaverse/scratch/optic-update/privileges/cleverllamas-mother-1.json
2cleverllamas-llamaverse/cleverllamas/llamaverse/scratch/optic-update/privileges/cleverllamas-mother-2.json
3xdmp:eval-in/cleverllamas/llamaverse/scratch/optic-update/privileges/cleverllamas-mother-3.json
4unprotected-collections/cleverllamas/llamaverse/scratch/optic-update/privileges/cleverllamas-mother-4.json

The formatted tab shows the same write output as a compact table. The written documents contain user, privilegeId, ordinality, privilegeName, and source. The key thing to notice is the shape of the update plan: op:unnest-inner() makes the privilege sequence row-wise, and op:execute() persists those rows as documents.

What to notice: op:execute() is intentionally different from op:result(). This plan mutates content by writing a privilege snapshot set, and op:unnest-inner() gives each privilege row an ordinality so you can see the flattening order directly in persisted output.

Mutation Note

The examples above split into two categories:

  1. op:xpath() plus expression libraries are read-oriented shaping tools.
  2. op:write() plus op:execute() are content-changing tools.

Keep those two modes separate in your mental model. Most Optic articles are about row logic; this section is about when row logic becomes an update program.

Building Pipelines?

Decision rule: keep read-shaping and mutation plans mentally separate; it prevents subtle side effects during maintenance.

Manipulation gets even more useful when you know where your data comes from and where it's headed:

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!