Skip to main content

Documentation Index

Fetch the complete documentation index at: https://developer.kodexa.ai/llms.txt

Use this file to discover all available pages before exploring further.

The hardest bug in a human-in-the-loop workflow is the one where a reviewer carefully fixes a value, clicks Accept, and then a few seconds later watches an event subscription or an AI enrichment quietly stomp on their edit. The fix is provenance: every attribute carries an ownerUri, and once it says user://, any automation downstream should treat the value as authoritative and step away. This recipe shows the full loop — how a click on Accept writes the ownership mark, how event subscriptions check for it before writing, and how AI/automation pipelines should defer to a user-owned value.

The problem

You have an extraction model that fills in vendor_id from a service bridge. A reviewer overrides the model’s value because they know the bridge sometimes confuses subsidiaries. They click Accept. Two seconds later an event subscription on vendor_name fires, calls the bridge again, and resets vendor_id back to the wrong answer. What’s missing is the contract: once a user has touched this value, downstream automation backs off. The platform already records the ownerUri of every attribute write — the cookbook below wires it into the rest of your system.

How attribute ownership works

Every data attribute carries an ownerUri indicating who last wrote it:
PrefixSet by
user://email@domainA reviewer in the UI (typing in an editor, clicking an action that writes attributes)
model://<modelId>The extraction engine when an extraction model produces the value
module://<moduleId>A processing module
The UI surfaces ownerUri.startsWith("user://") as the “Edited Value” indicator next to each control. That is the same prefix you use as the gate inside event scripts and modules.
You don’t have to call any special API to claim ownership for a user. Any write through currentObject.setAttribute(...) from the workspace sets ownerUri = user://<reviewer email> automatically. The canonical way to claim ownership of unchanged values when a reviewer clicks an action is the takeOwnershipForPaths action property described in Step 1.

Step 1 — Add takeOwnershipForPaths to the action

Pick the paths in the visual picker — when a reviewer clicks the action, every listed path will have its ownerUri flipped to user://<reviewer email> automatically. The change writes through the same audit-trail and delta-export pipeline as a regular reviewer edit, so manager review and second-tab clients see the claim.
metadata:
  actions:
    - uuid: accept-action
      type: accept
      label: "Accept"
      properties:
        targetStatus: approved
        statusSlug: approved
        icon: check
        color: green
        keybind: "a"
        onlyEnabledIfNoOpenExceptionsForPaths:
          - { taxonomySlug: invoice-taxonomy, taxonPath: invoice/total_amount }
          - { taxonomySlug: invoice-taxonomy, taxonPath: invoice/vendor_id }
        takeOwnershipForPaths:
          - { taxonomySlug: invoice-taxonomy, taxonPath: invoice/vendor_id }
          - { taxonomySlug: invoice-taxonomy, taxonPath: invoice/total_amount }
What this does at click time:
  1. The platform walks every listed { taxonomySlug, taxonPath } pair against the matching data objects on the task.
  2. For each matching attribute it sets ownerUri = user://<reviewer email>, even if the value itself is unchanged.
  3. The write goes through the standard audit-trail path, so the change shows up in the activity feed, in delta exports, and to any second-tab client subscribed to the document.
  4. The task is saved as part of the action; status transitions to approved.
The result is a hard, queryable contract: every path you listed is now stamped as user-owned, and every event subscription, AI agent, and module that reads ownerUri (Steps 2 and 3 below) will defer to the reviewer.
The action editor in Studio renders takeOwnershipForPaths and onlyEnabledIfNoOpenExceptionsForPaths as a polished picker with taxonomy filtering, path search, group-taxon support, and a custom-path fallback for paths not in any bound taxonomy — no need to type slugs by hand.
The legacy plain-string list (["invoice/vendor_id", "invoice/total_amount"]) is still accepted by the platform — both properties normalize string entries to { taxonomySlug: "", taxonPath: "..." } at runtime — but the object form is preferred and is what the Studio editor saves. Mixing both forms in a single list is supported during a migration.
Pair this with the gate from Gating actions on exceptions. The ownership write only happens when the reviewer was actually allowed to click — broken data never gets an approval signature.

Step 2 — Make event subscriptions respect user ownership

In a Data Definition, an event subscription script can read the ownerUri of any attribute it is about to overwrite. If the value already belongs to a human, skip the write.
- name: invoice
  group: true
  eventSubscriptions:
    - name: enrich-vendor-id-from-bridge
      on: "changed:dataAttribute:vendor_name"
      script: |
        if (!currentObject) return;

        // Defer to the reviewer if they have already taken ownership.
        var existingId = currentObject.getAttributeByName("vendor_id");
        if (existingId && (existingId.getOwnerUri() || "").indexOf("user://") === 0) {
          log.debug("vendor_id is user-owned, skipping enrichment");
          return;
        }

        var name = currentObject.getFirstAttributeValue("vendor_name");
        if (!name) return;

        var match = serviceBridge.call("/vendors/lookup", { name: name });
        if (match && match.id) {
          currentObject.setAttribute("vendor_id", match.id);
        }
  children:
    - name: vendor_name
      label: Vendor Name
      taxonType: STRING
    - name: vendor_id
      label: Vendor ID
      taxonType: STRING
The pattern is always the same: read the ownerUri first, bail if it starts with user://, otherwise proceed.
For a stricter rule, also bail if the accepted_by tracking attribute is non-empty — that handles the case where the reviewer did not touch vendor_id directly but did approve the enclosing object as a whole.

Step 3 — Make AI agents and modules respect user ownership

The same rule applies to anything writing through the Kodexa Document APIs from a module or agent step:
# Pseudocode in a Python module
attr = data_object.get_attribute_by_name("vendor_id")
if attr and (attr.owner_uri or "").startswith("user://"):
    # Reviewer already took ownership — leave it alone.
    return
data_object.set_attribute("vendor_id", lookup_vendor(name))
The shape of the check matches the event-subscription example. Make the guard the first thing the writer does, before any work that would be wasted.

Step 4 — Surface ownership in the UI

The data form renderer already shows an “Edited Value” indicator next to attributes whose ownerUri starts with user://. Reviewers therefore see, at a glance, which fields they (or takeOwnershipForPaths) have already taken responsibility for.

Optional — Surface who and when in the form

takeOwnershipForPaths flips ownerUri but does not record a separate timestamp or reviewer email — the audit trail has that information, but it isn’t bound into the form. If you want a manager-review or second-pass workflow to see the reviewer email and accept timestamp directly inside the data form, you can layer the older “tracking attribute” trick on top of takeOwnershipForPaths. Add an attributes block alongside takeOwnershipForPaths. Each entry tells the platform to write a value to a taxon when the action is clicked. Two valueType: metadata helpers exist specifically for this: currentUserEmail and currentTimestamp.
metadata:
  options:
    - name: accepted_by
      label: "Accepted By"
      type: text
      hint: "Set automatically when a reviewer clicks Accept."
    - name: accepted_at
      label: "Accepted At"
      type: date
      hint: "Set automatically when a reviewer clicks Accept."

  actions:
    - uuid: accept-action
      type: accept
      label: "Accept"
      properties:
        targetStatus: approved
        takeOwnershipForPaths:
          - { taxonomySlug: invoice-taxonomy, taxonPath: invoice/vendor_id }
          - { taxonomySlug: invoice-taxonomy, taxonPath: invoice/total_amount }
        attributes:
          - taxon:
              taxonomySlug: invoice-taxonomy
              taxonPath: invoice/accepted_by
            valueType: metadata
            metadataKey: currentUserEmail
          - taxon:
              taxonomySlug: invoice-taxonomy
              taxonPath: invoice/accepted_at
            valueType: metadata
            metadataKey: currentTimestamp
Bind the form controls for accepted_by and accepted_at as read-only fields. They populate the moment the first reviewer clicks Accept, and the next reviewer can see at a glance who came before them. This pattern is purely additive — the attributes block writes the tracking values, and takeOwnershipForPaths does the actual ownership claim. Drop the attributes block if you don’t need the form-level visibility; the ownership contract still holds.

What to do when ownership needs to be cleared

Sometimes a reviewer’s previous decision needs to be reset — a downstream system rejected the document, or a different reviewer is taking over. Two options:
  1. Add a “Re-open” action that does not set takeOwnershipForPaths and (if you used the tracking-attribute pattern) clears accepted_by and accepted_at via the same attributes block with empty values. Reviewers in the next pass will see clean fields. Note that ownerUri from the original Accept stays in the audit trail; the next user write naturally replaces the live value.
  2. Add an “Override” action that writes a different ownership marker (for example accepted_by_supervisor) and updates a status to re-review. This preserves the original signature for audit while moving the workflow forward.
Either approach is a normal task-template action — the difference is just what the action’s properties write.

Recipe summary

  1. On the Accept action, list the critical paths under takeOwnershipForPaths using the Studio picker — { taxonomySlug, taxonPath } objects are the canonical form.
  2. In every event subscription that may overwrite a reviewer’s edit, read getOwnerUri() first and bail when it begins with user://.
  3. Apply the same guard in any module or agent code that writes the same fields.
  4. Optional: layer an attributes block on top to record accepted_by / accepted_at for in-form visibility.
  5. Optional: add a Re-open action for workflows that need to restart.

See also