Metadata mappings
Metadata mappings let you populate controlled metadata properties directly from the uncontrolled metadata already attached to a document — deterministically, without calling a language model. You define a list of rules inline in your workflow; where a rule's condition is met, it writes a predefined value into the document's results.
This is handled by the MetadataMapper processing unit (processors.extractors.MetadataMapper). Rules are evaluated at runtime against DocumentVersion.metadata().
When to use this
Documents from source systems often already carry governed information in their native fields — for example a ProjectWise environment attribute env_LOCATOR_CODE = "L01". If a field reliably encodes a classification, mapping it with a rule is faster, free and fully auditable compared with asking a classifier to re-derive it.
Uncontrolled metadata is offered up as-is, with no guarantees on its veracity (see Documents). Mapping rules are the deterministic bridge from those raw fields to your governed standard.
Rule structure
A rule pairs a condition (tested against one or more source metadata fields) with one or more writes (the controlled properties to set when the condition is met).
{
"condition": { <condition-group-or-leaf> },
"writes": [ { "target_id": "<property-id>", "value": <payload> } ],
"certainty": "high",
"explanation": "Mapped from existing document metadata"
}
| Attribute | Required | Type | Description |
|---|---|---|---|
condition |
Yes | Object | A single leaf condition, or a group of leaves combined with all/any. See Conditions. |
writes |
Yes* | Array | One or more { target_id, value } writes applied when the condition matches. See Writes. |
certainty |
String | Default certainty for this rule's writes. Defaults to "high". |
|
explanation |
String | Default explanation stamped on this rule's writes. Defaults to "Mapped from existing document metadata". |
Single-write shorthand
Instead of a writes array you may put target_id and value directly on the rule for the common one-property case. The two forms below are equivalent:
{ "condition": {...}, "target_id": "doc-type", "value": { "code": "CO", "description": "Correspondence" } }
{ "condition": {...}, "writes": [ { "target_id": "doc-type", "value": { "code": "CO", "description": "Correspondence" } } ] }
Writes
Each write names the property to set and the value to write.
| Attribute | Required | Type | Description |
|---|---|---|---|
target_id |
Yes | String | The id of the controlled MetadataProperty to write. |
value |
Yes | Object or String | The predefined value. For a classification property use { "code": ..., "description": ... }; for a freetext/text property use a string. String fields may contain regex capture references. |
certainty |
String | Overrides the rule-level certainty for this write. |
|
explanation |
String | Overrides the rule-level explanation for this write. |
Conditions
A leaf condition binds one source metadata key (source_id) to one test (type). The available tests:
type |
Fields | Matches when |
|---|---|---|
regex |
pattern |
pattern is found anywhere in the (stringified) metadata value. Matching is case-insensitive. Capture groups can feed the written value. |
numeric_range |
min, max, inclusive |
The value parses as a number and falls within the bound(s). min and max are each optional. |
equals |
value |
The metadata value equals value (compared as strings). |
one_of |
values |
The metadata value is one of values (compared as strings). |
exists |
— | The source_id key is present at all (any value, including empty). |
{ "source_id": "env_LOCATOR_CODE", "type": "regex", "pattern": "L\\d\\d" }
{ "source_id": "env_REVISION", "type": "numeric_range", "min": 0, "max": 100, "inclusive": "both" }
{ "source_id": "env_STATUS", "type": "equals", "value": "A" }
{ "source_id": "env_LOCATOR_CODE", "type": "one_of", "values": ["L01", "L02"] }
{ "source_id": "env_LOCATOR_CODE", "type": "exists" }
A leaf whose source_id is not present in the document's metadata simply does not match. Regex matching is case-insensitive — L\d\d matches both L07 and l07.
Numeric bounds and inclusivity
For numeric_range, the inclusive field controls which bounds are closed:
inclusive |
Interval | Meaning |
|---|---|---|
"both" (default) |
[min, max] |
Both bounds inclusive. |
"min" |
[min, max) |
Lower inclusive, upper exclusive. |
"max" |
(min, max] |
Lower exclusive, upper inclusive. |
There is deliberately no option for a fully open interval — at least one bound is always inclusive. Omit min or max for a one-sided range.
Combining conditions across fields
To test more than one source field, wrap leaves in a group with a match combinator:
| Attribute | Required | Type | Description |
|---|---|---|---|
match |
String | "all" (every leaf must match — AND) or "any" (at least one — OR). Defaults to "all". |
|
conditions |
Yes | Array | The leaf conditions to combine. |
{
"match": "all",
"conditions": [
{ "source_id": "env_LOCATOR_CODE", "type": "regex", "pattern": "L\\d\\d" },
{ "source_id": "env_STATUS", "type": "equals", "value": "A" }
]
}
Single condition
A rule's condition may be a lone leaf (no match/conditions wrapper) — it behaves as a group of one.
Capturing segments with regex
A regex condition can capture part of the source value and splice it into the written value. Reference captures with Python backreference syntax — numbered (\1, \2) or named (\g<name>):
{
"condition": { "source_id": "env_LOCATOR_CODE", "type": "regex", "pattern": "L(?P<seq>\\d\\d)" },
"writes": [
{ "target_id": "doc-type", "value": { "code": "CO-\\g<seq>", "description": "Correspondence \\g<seq>" } }
]
}
With env_LOCATOR_CODE = "L07" this writes { "code": "CO-07", "description": "Correspondence 07" }.
Use backslashes, not $, for capture references
Capture references use \1 / \g<name> (written as "\\1" / "\\g<name>" in JSON). Do not use $1 — the workflow runner resolves any $-prefixed string as a context variable before the rule is evaluated, so a $-style reference would be lost.
Multiple regex fields → use named groups
When a rule conditions on more than one regex field, numbered backreferences are ambiguous. Use named groups ((?P<name>...) → \g<name>) so references resolve unambiguously across fields.
Ordering: last write wins
Rules are evaluated top to bottom. When more than one write targets the same property, the one that appears later in the list wins. Writes to different properties all apply. This lets you layer a specific override beneath a general default:
[
{ "condition": { "source_id": "env_LOCATOR_CODE", "type": "exists" },
"target_id": "doc-type", "value": { "code": "UN", "description": "Unclassified" } },
{ "condition": { "source_id": "env_LOCATOR_CODE", "type": "regex", "pattern": "L\\d\\d" },
"target_id": "doc-type", "value": { "code": "CO", "description": "Correspondence" } }
]
Here any document with a locator code starts as Unclassified, but one matching L\d\d ends up as Correspondence because that rule is lower in the list.
Respecting existing results
The mapper reuses the standard use_persisted_results flag, but for this unit it defaults to true:
use_persisted_results |
Behaviour |
|---|---|
true (default) |
Respect existing. A write only lands on a property that has no value yet. Values from a previous run — or written by an earlier stage of this run — are left untouched. |
false |
Overwrite. Writes always land, replacing any existing value for that property. |
Set it via the step's instance_params.
Stage ordering matters
"Existing" means whatever is in the document's results when the mapper runs. Because steps within a stage run in parallel, place MetadataMapper in its own stage relative to any unit whose results it should respect or override.
Validation
If a target_id refers to a classification property, the written code is validated against that property's allowed values. An invalid code is still written — so it surfaces during QC — but flagged as not format-valid. Writes to freetext/text properties are not code-validated.
A target_id must resolve to a property your standard knows about (a governed property or a custom column). If it doesn't, that write is skipped with a warning rather than creating an ungoverned value — and if a rule has no valid targets left, the rule is skipped entirely without evaluating its condition.
Full example
A stage that applies two rules — a multi-field, multi-target rule and a simpler override:
{
"name": "Map metadata",
"steps": [
{
"name": "Apply metadata mapping rules",
"class": "processors.extractors.MetadataMapper",
"method": "run",
"instance_params": { "use_persisted_results": "$use_persisted_results" },
"call_params": {
"document_version": "$connection.document_version",
"session": "$connection.session",
"rules": [
{
"condition": {
"match": "all",
"conditions": [
{ "source_id": "env_LOCATOR_CODE", "type": "regex", "pattern": "L\\d\\d" },
{ "source_id": "env_STATUS", "type": "equals", "value": "A" }
]
},
"writes": [
{ "target_id": "doc-type", "value": { "code": "CO", "description": "Correspondence" } },
{ "target_id": "status", "value": { "code": "WIP", "description": "Work in progress" } }
],
"certainty": "high",
"explanation": "Mapped from existing document metadata"
},
{
"condition": { "source_id": "env_LOCATOR_CODE", "type": "one_of", "values": ["D01", "D02"] },
"target_id": "doc-type",
"value": { "code": "DR", "description": "Drawing" }
}
]
}
}
]
}