{"id":79335,"plugin_id":"plugins_6ae9fc38af0481918725c125cf7c1ebd","kind":"skill","collection_source":"plugin_package","comparison_source":null,"observed_at":"2026-11-03T19:28:24.160Z","digest":"40251098da3a759df732e9f910d8603dd1c83d463c3968fe2f7c55cead2ed82e","against":null,"payload":{"description":"Designs and operates Boomi Data Hub master data — model and source design, data quality steps (also called data quality rules), deployment lifecycle, quarantine triage, and golden-record CRUD including end-dating (soft delete), restore, and un-delete — when the user works with Boomi Data Hub (MDM) configuration or stewardship. Purge (permanent deletion) is out of scope. Pairs nicely with the boomi-integration skill.","included_files":[{"relative_path":"CHANGELOG.md","size_in_bytes":4092},{"relative_path":"LICENSE","size_in_bytes":1311},{"relative_path":"README.md","size_in_bytes":1863},{"relative_path":"VERSION","size_in_bytes":6},{"relative_path":"references/data_quality_step.md","size_in_bytes":8595},{"relative_path":"scripts/datahub-common.sh","size_in_bytes":7049},{"relative_path":"scripts/datahub-connection.sh","size_in_bytes":8647},{"relative_path":"scripts/datahub-deployment.sh","size_in_bytes":2021},{"relative_path":"scripts/datahub-env-check.sh","size_in_bytes":2586},{"relative_path":"scripts/datahub-golden-record.sh","size_in_bytes":5552},{"relative_path":"scripts/datahub-model.sh","size_in_bytes":12955},{"relative_path":"scripts/datahub-quarantine.sh","size_in_bytes":3485},{"relative_path":"scripts/datahub-repository.sh","size_in_bytes":2401},{"relative_path":"scripts/datahub-source.sh","size_in_bytes":4557}],"name":"boomi-datahub","skill_md_contents":"---\nname: boomi-datahub\ndescription: Designs and operates Boomi Data Hub master data — model and source design, data quality steps (also called data quality rules), deployment lifecycle, quarantine triage, and golden-record CRUD including end-dating (soft delete), restore, and un-delete — when the user works with Boomi Data Hub (MDM) configuration or stewardship. Purge (permanent deletion) is out of scope. Pairs nicely with the boomi-integration skill.\n---\n\n# boomi-datahub\n\n## Scope\n\nIn: model design; source configuration; data quality steps; model lifecycle (Draft → Published → Deployed); quarantine triage; repository operations; golden-record CRUD, end-dating, and restore.\n\nOut: building Boomi integration processes — those belong to `boomi-integration`. Integration processes that interact with Data Hub are `boomi-integration`'s territory; the connection they need — a REST client on the repository API, or the Boomi Data Hub connector's own — can be bootstrapped from this workspace's `.env` via `datahub-connection.sh bootstrap`.\n\n## API surfaces\n\nData Hub exposes two API surfaces. **Always reach them through the CLI tools in § Scripts inventory — never hand-roll curl, URLs, or auth headers.** The scripts build the URLs, route to the correct surface, and read credentials from `.env`; constructing your own request is how the wrong-surface / wrong-auth mistakes happen.\n\n- **Platform API** — account-level admin: models, sources, repositories, clouds, deployment.\n- **Repository API** — per-repository: golden records, stewardship/quarantine, deployed-universe listing. It is **XML-only** on both request and response, so the payloads you author for these scripts are always XML (the Platform API speaks JSON on most reads).\n\nA **Hub Cloud** is the runtime host that holds repositories (along with their deployed models and golden records) — not an API name. Multiple repositories can live on one Hub Cloud.\n\n## Credentials\n\nThe skill reads credentials from the workspace `.env`. Sensitive values are never accepted as command-line arguments — the scripts source them from `.env` directly.\n\n| Surface | `.env` keys |\n|---|---|\n| Platform API | `BOOMI_USERNAME`, `BOOMI_API_TOKEN`, `BOOMI_ACCOUNT_ID`, `BOOMI_API_URL` |\n| Repository API | `DATAHUB_REPO_URI`, `DATAHUB_REPO_USERNAME`, `DATAHUB_REPO_AUTH_TOKEN` |\n\nSetting the Repository API keys is the opt-in for golden-record operations.\n\n## Repository-scoped operations\n\nA repository is the unit of deployment: models, sources, channels, staging areas, golden records, and quarantine all live inside one. Repository CLI tools target its base URL (e.g. `https://c01-usa-east.hub-test.boomi.com/mdm`); account-level admin uses the Platform API.\n\nThat base URL is a **Hub Cloud host**, shared by every repository on it — so the URL alone never identifies a repository. The auth pair does: `DATAHUB_REPO_USERNAME` is `<account-id>.<repo-token-id>` (suffix is repo-specific), paired with that repository's Hub Authentication Token. A URL match with the wrong token authenticates fine but lands on a different repository's universe registry — the confusing \"universe does not exist\" error. **Never reuse a REST client connection (or any Data Hub artifact) on URL match alone** — confirm the token targets the intended repository, or wire a new connection to it - discuss with the user if in any doubt.\n\n## Model lifecycle: Draft → Published → Deployed\n\n| State | Mutable? | What freezes |\n|---|---|---|\n| Draft | Yes — fields, types, repeatability, match rules can all change | Nothing |\n| Published | Partly — new fields can be added (the update lands in a new draft to republish) | Type and repeatability of **existing** fields are locked; adding new fields is not |\n| Deployed | Runtime-bound — attached to a repository, ingesting and merging data | Undeploy is required before structural changes propagate |\n\nOnce deployed, the **universe ID equals the model ID** (`mdm:universeId` in the deploy response is the same GUID as the model's `mdm:id`). The deployment ID is a separate value. Scripts that take `<universe-id>` should be given the model ID.\n\nThe Platform API's `/models` resource exposes only `publicationStatus` (boolean — draft vs published). Deploy is a separate binding of a published model to a repository (via Deploy Universe) and is not a filter value on `/models`. Query deployment via the repository's universe-deployment status.\n\n**Draft-only models require `--draft`.** `model.sh get`/`pull` target the published version by default; a never-published model has none, so pass `--draft` — otherwise the API returns a misleading HTTP 400 \"… is not a valid component ID\".\n\n**`get` is not a deletion liveness check.** After a successful `delete`, `model.sh get`/`pull` by ID still returns HTTP 200 with the full model XML — the Platform API retains the schema of deleted models and only excludes them from `list`. To confirm a model is deleted, use `list` (the model will be absent) or a second `delete` (returns \"A model with ID … does not exist\"). This applies to models only; source `get` on a deleted ID correctly returns HTTP 400.\n\n**Source attachment** transitions `SOURCE_ATTACHMENT_REQUESTED` → `SOURCE_ATTACHED` → `ENABLE_INITIAL_LOAD_REQUESTED` → `INITIAL_LOAD_ENABLED` → `FINISH_INITIAL_LOAD_REQUESTED` → `INITIAL_LOAD_FINISHED`. The `*_REQUESTED` states are async, so `enable-initial-load` and `finish-initial-load` **self-gate** — each blocks until the source reaches the state that makes the next step safe, so just run the lifecycle steps in order; no manual `source.sh status` polling is needed. `enable-initial-load` waits out the async `SOURCE_ATTACHMENT_REQUESTED` state before it acts and returns only once the source is `INITIAL_LOAD_ENABLED` (so a following upsert can't fire too early); it does two sequential waits, so it can block up to ~2 min. `finish-initial-load` returns only once the source is `INITIAL_LOAD_FINISHED` (a single wait, ≤60s).\n\n**Only one source at a time can be in initial-load.** Calling `enable-initial-load` against a second source while another is mid-load returns HTTP 400 \"Cannot perform an initial load on more than one source at a time.\" Run `finish-initial-load` on the active source first; because it blocks until that source reaches `INITIAL_LOAD_FINISHED`, the next source's `enable-initial-load` is safe to call immediately after.\n\n## Field types and structures\n\n| Aspect | Valid values |\n|---|---|\n| `<mdm:field type=\"...\">` | `BOOLEAN`, `CLOB`, `DATE`, `DATETIME`, `ENUMERATION`, `FLOAT`, `INTEGER`, `REFERENCE`, `STRING`, `TIME` |\n| Source `<mdm:channelUpdatesFields>` | `All`, `Changed` |\n\n**STRING** fields have `maxLength` ≤255 (server-enforced). Use `CLOB` for longer text.\n\n**ENUMERATION** declares allowed values via child `<mdm:value>` elements (one per value).\n\n**REFERENCE** is a native cross-model link type. Attributes: `referenceUniverseId=\"<other-model-id>\"`, `incomingReferenceIntegrity=\"true|false\"`, `outgoingReferenceIntegrity=\"true|false\"`. In batch upserts, the field element's text content is the *source-side* natural key of the target record (e.g. `<parentRef>parent-1</parentRef>`); the platform resolves it to the parent's golden-record GUID, and that's what subsequent query responses return. With `incomingReferenceIntegrity=\"true\"`, an unresolvable target → 202 + silent quarantine with `cause=REFERENCE_UNKNOWN`. No STRING-by-convention workaround is needed. A `required=\"true\"` REFERENCE field forces `outgoingReferenceIntegrity=\"true\"` — the platform rejects model creation otherwise (\"OutgoingReferenceIntegrity must be true when REFERENCE field … is required\").\n\n**`id` is reserved at the model root.** Defining a field with `name=\"id\"` at the root fails with \"A user-defined 'id' field exists at the root level.\" The platform auto-provisions the entity id (the `<id>` value sent in batch upserts). Use a different name for your natural-key field (e.g. `recordId`).\n\n**Field groups** wrap related fields in `<mdm:fieldGroup name=\"...\" uniqueId=\"...\" repeatable=\"...\" required=\"...\">...</mdm:fieldGroup>` inside `<mdm:fields>`. `repeatable` is locked at publish.\n\n- **Non-repeating** (`repeatable=\"false\"`): one instance. The group's `name` is the wrapper element in batch upserts (`<address>…</address>`). Sending the subfields flat, or sending a second instance, → silent quarantine `PARSE_FAILURE`.\n- **Repeating** (`repeatable=\"true\"`) is a **collection**. Model-create fails (\"Missing required properties for collection field\") unless the `<mdm:fieldGroup>` also carries `collectionTag`, `collectionUniqueId`, `identifyBy=\"KEY\"`, and `collectionKeys` (the key field's `uniqueId`). In batch upserts and query responses it is **double-wrapped** — the `collectionTag` element once, wrapping repeated `name` elements:\n\n```xml\n<mdm:fieldGroup name=\"address\" uniqueId=\"ADDRESS\" repeatable=\"true\" required=\"false\"\n                collectionTag=\"addresses\" collectionUniqueId=\"ADDRESSES\" identifyBy=\"KEY\" collectionKeys=\"CITY\">\n    <mdm:field name=\"city\" uniqueId=\"CITY\" type=\"STRING\" maxLength=\"100\" repeatable=\"false\" required=\"false\"/>\n</mdm:fieldGroup>\n<!-- batch upsert / query: <addresses><address><city>…</city></address><address>…</address></addresses> -->\n```\n\nA **repeating single field** (`<mdm:field repeatable=\"true\">`, no surrounding group) is the same collection mechanism: it needs `collectionTag`/`collectionUniqueId`/`identifyBy=\"KEY\"` (but not `collectionKeys` — it keys on its own value) and double-wraps identically — `<phones><phone>v1</phone><phone>v2</phone></phones>`, the inner element carrying the scalar value. Either way, exceeding a non-repeating field/group's single instance → quarantine `PARSE_FAILURE`.\n\n**Model name normalization.** On create, the platform lowercases and strips non-alphanumerics from `<mdm:name>` (e.g. `MyModel` → `mymodel`, `Customer(Boomeus)` → `customerboomeus`). The normalized form is what batch upserts must use as the entity wrapper element. Re-fetch the model via `model.sh get` if uncertain.\n\n## Match rules\n\nA model needs at least one match rule to publish. Rules evaluate in document order; the first satisfied rule wins. Order most-restrictive first.\n\nA rule (`<mdm:matchRule topLevelOperator=\"AND|OR|NOT\">`) holds any mix of:\n\n- `<mdm:simpleExpression>` — one `<mdm:fieldUniqueId>`; incoming-vs-existing equality on that field.\n- `<mdm:advancedExpression>` — `<mdm:ruleOperator>` `EQUALS` | `STRICT_EQUALS` (case-sensitive) | `IS_SIMILAR_TO` (fuzzy; add `<mdm:similarityAlgorithm>` and `<mdm:tolerance>`), then two inputs as in the sample below. `<mdm:inputType>` is `INCOMING` | `EXISTING` | `STATIC` (`STATIC` compares a literal `<mdm:value>`); the two inputs may name different fields.\n- `<mdm:expressionGroup operator=\"AND|OR|NOT\">` — nests arbitrarily.\n\nAlgorithms (exact literals): `Jaro-Winkler`, `Levenshtein`, `Bigram`, `Trigram`, `Soundex`. Tolerance 0.0–1.0 (`Soundex`: 0–4).\n\n```xml\n<mdm:matchRules>\n    <mdm:matchRule topLevelOperator=\"AND\">\n        <mdm:simpleExpression><mdm:fieldUniqueId>EMAIL</mdm:fieldUniqueId></mdm:simpleExpression>\n    </mdm:matchRule>\n    <mdm:matchRule topLevelOperator=\"AND\">\n        <mdm:simpleExpression><mdm:fieldUniqueId>POSTAL</mdm:fieldUniqueId></mdm:simpleExpression>\n        <mdm:advancedExpression>\n            <mdm:ruleOperator>IS_SIMILAR_TO</mdm:ruleOperator>\n            <mdm:similarityAlgorithm>Jaro-Winkler</mdm:similarityAlgorithm>\n            <mdm:tolerance>0.85</mdm:tolerance>\n            <mdm:firstInput>\n                <mdm:inputType>INCOMING</mdm:inputType>\n                <mdm:fieldUniqueId>LASTNAME</mdm:fieldUniqueId>\n            </mdm:firstInput>\n            <mdm:secondInput>\n                <mdm:inputType>EXISTING</mdm:inputType>\n                <mdm:fieldUniqueId>LASTNAME</mdm:fieldUniqueId>\n            </mdm:secondInput>\n        </mdm:advancedExpression>\n    </mdm:matchRule>\n</mdm:matchRules>\n```\n\n- **`create` checks shape; `publish` checks semantics.** A draft holds rules that publish rejects: fuzzy expressions not grouped with an exact expression, `NOT` with more than one child, fuzzy on a non-text field.\n- **`NOT` inverts its child** — a standalone NOT rule matches *dissimilar* records; only use it inside a group alongside positive expressions.\n- **Pulled rules are canonicalized** — same-field EQUALS collapses to a simpleExpression, omitted tolerance becomes `0.0`, `4` → `4.0`. Re-pull after create before editing.\n- **Tune with `datahub-golden-record.sh match`** — see § Match request for its diagnostic payload.\n\n## Tags\n\nA tag classifies golden records by a business rule. A model's tags are optional (`<mdm:tags/>` is valid); the source `sendFilter` below can reference a tag by name to gate a source's outbound channel. Each `<mdm:tag>` in the top-level `<mdm:tags>` block wraps a `<mdm:businessRule>` of keyed inputs + nested conditions:\n\n```xml\n<mdm:tags>\n    <mdm:tag name=\"Customer\">\n        <mdm:businessRule name=\"Customer\">\n            <mdm:inputs>\n                <mdm:input key=\"1\" alias=\"Stage\"    fieldUniqueId=\"STAGE\"    type=\"Field\"/>\n                <mdm:input key=\"2\" alias=\"Category\" fieldUniqueId=\"CATEGORY\" type=\"Field\"/>\n            </mdm:inputs>\n            <mdm:conditions topLevelOperator=\"OR\">\n                <mdm:conditionGroup operator=\"AND\">\n                    <mdm:condition operator=\"EQUAL\">\n                        <mdm:firstInput  type=\"Field\"  key=\"1\"/>\n                        <mdm:secondInput type=\"Static\" value=\"Active\" key=\"0\"/>\n                    </mdm:condition>\n                    <mdm:condition operator=\"IS_NOT_EMPTY\">\n                        <mdm:firstInput type=\"Field\" key=\"2\"/>\n                    </mdm:condition>\n                </mdm:conditionGroup>\n            </mdm:conditions>\n        </mdm:businessRule>\n    </mdm:tag>\n</mdm:tags>\n```\n\n- **Omit `id` on create** (platform generates a GUID; `pull` to capture it); a supplied `id` is honored on update. Empty `<mdm:tags/>` removes all tags.\n- Conditions reference inputs by `key` (need not be contiguous); `<mdm:conditionGroup operator=\"AND|OR\">` nests arbitrarily under `<mdm:conditions topLevelOperator=\"AND|OR\">`. Binary operators need `<mdm:secondInput>` (`type=\"Field\" key=\"N\"` or `type=\"Static\" value=\"...\" key=\"0\"`); empty-checks take `firstInput` only.\n- Operators: `EQUAL`, `NOT_EQUAL`, `LESS_THAN`, `GREATER_THAN`, `LESS_OR_EQUAL`, `GREATER_OR_EQUAL`, `CONTAINS`, `NOT_CONTAINS`, `STARTS_WITH`, `IS_EMPTY`, `IS_NOT_EMPTY`. Note the short `*_OR_EQUAL` (not `*_THAN_OR_EQUAL`) and `NOT_CONTAINS` (not `DOES_NOT_CONTAIN`).\n\n### Outbound gating — `sendFilter`\n\nA source's `<mdm:outbound>` references a tag by **`name`** to gate which records publish on its channel:\n\n```xml\n<mdm:sendFilter scope=\"All\">\n    <mdm:tags><mdm:tag name=\"Customer\"/></mdm:tags>\n</mdm:sendFilter>\n```\n\n- `scope` is `All` or `Creates`. Multiple tags allowed; `id` is optional (only `name` is required).\n- The `name` must resolve to a defined tag (else HTTP 400 \"… does not exist in the model\") — define the tag first.\n\n### Tag input functions\n\nA tag input can be a function that transforms a field. The input wraps `<mdm:function>`, which maps fields as parameters and declares its outputs:\n\n```xml\n<mdm:input key=\"3\" alias=\"String Split\" type=\"Function\">\n    <mdm:function type=\"StringSplit\" splitBy=\"Delimiter\" delimiter=\"@\">\n        <mdm:inputs><mdm:input name=\"Original String\" uniqueId=\"EMAIL\"/></mdm:inputs>\n        <mdm:outputs><mdm:output name=\"out1\"/><mdm:output name=\"out2\"/></mdm:outputs>\n    </mdm:function>\n</mdm:input>\n```\n\n- Each function `<mdm:input>` `uniqueId` must be a non-empty, valid field `uniqueId`; `uniqueId=\"\"` is rejected. Multi-output functions declare `<mdm:outputs>` with named `<mdm:output>` children.\n- `type` tokens differ from the UI label and aren't derivable from it (e.g. *Right Character Trim* is `RightTrim`). By category:\n    - **String:** `LeftTrim`, `RightTrim`, `WhiteSpaceTrim`, `StringAppend`, `StringPrepend`, `StringConcat` (`delimiter`, `fixToLength`), `StringReplace`, `StringRemove`, `StringToLower`, `StringToUpper`, `StringSplit` (`splitBy`, `delimiter`)\n    - **Numeric:** `MathAbsoluteValue`, `MathAdd`, `MathDivide`, `MathCeiling`, `MathFloor`, `MathSetPrecision`, `NumberFormat`, `RunningTotal`, `Sum`, `Count`, `LineItemIncrement`, `SequentialValue`\n    - **Date:** `DateFormat`, `GetCurrentDate` (no inputs)\n- A condition tests a single-output function's result with a bare reference — `<mdm:firstInput type=\"Function\" key=\"N\"/>`, no output attribute. A `StringSplit` output is testable only when it is the function's **only** declared output — reference it with `outputFieldName=\"<output name>\"`; with two or more declared outputs the condition is rejected on `update`. To test multiple segments, use one single-output `StringSplit` input per segment.\n\n**Platform API known issues — halt and raise with the user; never work around silently:**\n\nIf this instruction remains in the skill the defect has not yet been resolved and this section remains critical. Boomi is aware of these issues and a fix is in progress; no support ticket is needed. You do not need to proactively raise them to the user unless their project or model is actively affected.\n\n- The API accepts both `MathSubtract` and `MathMultiply` but stores and executes them incorrectly — a rule authored as one function can be stored as a different one. **Never** author these.\n- For the same reason, treat `MathSubtract` and `LeftTrim` tag functions in any pulled model as suspect, even when UI-authored.\n- A pulled model may contain a condition on a multi-output `StringSplit`. Pushing that model back is rejected. Such tags must be edited in the UI, or restructured to single-output `StringSplit` inputs — never delete or restructure the user's condition without their explicit sign-off.\n\nThese configurations are niche, but if you encounter any of the above situations — STOP and raise it with the user.\n\n## Data quality steps\n\n`<mdm:dataQualitySteps>` holds `<mdm:step>` children that gate ingestion. A failing entity quarantines with `cause=ENRICH_ERROR` (see § Quarantine) instead of becoming a golden record. Steps are additive and ordered.\n\n`type` takes exactly two values; a step carrying neither is an ordinary quality service:\n\n| Kind | `type` | Authorable through the API |\n|---|---|---|\n| Business rule | `BUSINESS_RULE` | Yes — rule lives in the model XML, reusing § Tags' `<mdm:businessRule>` structure plus `<mdm:errorMessage>` |\n| Integration process call | `PROCESS` | Yes — the process must be a deployed Hub listener |\n| Ordinary — D&B or Loqate quality service | *absent* | No — UI only |\n\n**A model carrying an ordinary step cannot be updated through the API at all.** Any update submitting that step is rejected (`Missing required properties for data quality step <name>`), so every edit to such a model — fields, sources, match rules, tags — has to happen in the UI. Run `quality-steps` before planning a model edit; a `<mdm:step>` with no `type` means stop and route the user to the UI.\n\n**Deploy activates a step, not publish.** A step has no effect until its model version is republished *and* redeployed.\n\n`datahub-model.sh quality-steps` prints the block; `add-quality-step` and `remove-quality-step` edit it. Writes always produce a draft, so every edit after the first needs `--draft`.\n\nFull shapes and per-kind failure modes: `references/data_quality_step.md`.\n\n## End-dating and restore\n\nSoft deletion: the record leaves the active set but keeps fields, history, and source links, and can be restored. Purge (permanent) is out of scope.\n\n**`query` returns active records only** — an end-dated record yields `<RecordQueryResponse resultCount=\"0\" totalCount=\"0\"/>`, identical to a filter matching nothing. `totalCount` counts active only. Use `query-enddated` (same `RecordQueryRequest` body). `get`, `meta`, `history`, `get-by-source` still return end-dated records in full.\n\n| Mechanism | Command | `history` `enddatesource` |\n|---|---|---|\n| Direct | `enddate` / `enddate-bulk` | `*MDM*` |\n| Source-driven | `update` batch, `op=\"DELETE\"` on entity root | contributing source id |\n\n`enddate-bulk` takes `<RecordEndDateRequest>` in two forms that behave differently:\n\n| Form | Status | Body |\n|---|---|---|\n| ≤100 `<recordId>` | 200 | one `<result>` per record; `<message>` only when `<success>false</success>` |\n| `<filter>` | 202 | **empty** — applied asynchronously, seconds later |\n\nOver 100 ids → HTTP 400 `Cannot end-date more than 100 records at one time.`; the count is checked before the ids are validated, and unknown ids fail per-record rather than failing the batch. **The filter form's 202 is acceptance, not completion** — it returns nothing to confirm against, and acceptance does not guarantee application. Always verify with `query` / `query-enddated`.\n\n**Source-driven end-dating is one-way for the source.** Resending the entity returns HTTP 202 and quarantines `RECORD_ALREADY_ENDDATED` (`approve` → HTTP 400). A pipeline that doesn't poll quarantine looks healthy while dropping every later record for that entity. `restore` reverses the end-date but does not resolve the entry — it stays ACTIVE until the source resends, and that resend then incorporates normally (`<resolution>INCORPORATE_SUCCESS</resolution>`).\n\n`op=\"DELETE\"` is positional — on the **entity root** it end-dates the record and skips field validation (`<id>` alone is accepted despite `required=\"true\"` fields); on a collection **item** element (the `fieldGroup` name, e.g. `<phone>`) it removes just that item; on the **`collectionTag`** wrapper (e.g. `<phones>`) it is rejected — `PARSE_FAILURE`, `Attribute 'op' is not allowed to appear in element '…'`.\n\n**Restore is keyed on the source entity, not the record id** — `restore --source <source-id> <entity-id>`, both from `meta`'s `<link>`. Empty body on success. Record id, `createdDate`, and `establishedDate` survive; restore adds a history version rather than rewriting the end-dated one.\n\n### End-date timestamp: two formats\n\n| Surface | Attribute | Example |\n|---|---|---|\n| `meta` | `endDate` | `2026-08-18T17:17:17Z` (ISO-8601 UTC) |\n| `get`, `history`, `get-by-source` | `enddate` | `08-18-2026T12:17:17.000-0500` (MM-DD-YYYY, local offset) |\n\nNames differ by casing alone. The `get` form has a `T` but is month-first, not ISO-8601 — misparses silently when the day is ≤12. Absence of the attribute is the state flag; restore removes it, never blanks it. Origin timestamp is `startdate` in `history`, `createddate` in `get`, `createdDate` in `query`/`meta`.\n\n**Repository summary counters never recover.** `datahub-repository.sh get <repository-id> --universe <id>` decrements on an end-date but does not increment on a restore, so the error accumulates one record per cycle and does not self-correct. `query` + `query-enddated` are authoritative.\n\n## Quarantine\n\nFailed ingests do not enter golden state. They land in quarantine with one of these causes:\n\n| Cause | When it fires | Resolution |\n|---|---|---|\n| `PARSE_FAILURE` | XSD schema mismatch — wrong field names (`uniqueId` instead of field `name`), missing field-group wrapper, unknown elements | `delete`; fix payload, resubmit |\n| `FIELD_FORMAT_ERROR` | Value not in an `ENUMERATION`'s list, malformed `DATETIME`/`FLOAT`/`INTEGER`, etc. | `delete`; fix payload, resubmit |\n| `REQUIRED_FIELD` | Missing `required=\"true\"` fields | `delete`; fix payload, resubmit |\n| `REFERENCE_UNKNOWN` | A `REFERENCE` value doesn't resolve (when `incomingReferenceIntegrity=\"true\"`) | `delete`; fix payload, resubmit |\n| `POSSIBLE_DUPLICATE` | Match rules flagged a possible duplicate of an existing golden record | `reject` — discards the record, no merge |\n| `ENRICH_ERROR` | An entity failed a data quality step (see § Data quality steps) | `delete`; fix the payload or rule, resend from source. Not approvable or rejectable. UI-only Retry / Ignore exist while the deployed version is unchanged |\n| `RECORD_ALREADY_ENDDATED` | A source re-sent an entity whose golden record is end-dated | `datahub-golden-record.sh restore` (see § End-dating), then resend — restore alone leaves the entry ACTIVE; or `delete` to keep the record end-dated |\n| `REQUIRES_END_DATE_APPROVAL` | A source with `endDateApproval required=\"true\"` sent `op=\"DELETE\"` | `approve` — end-dates the record, attributed to that source |\n\n`ENRICH_ERROR`'s `<reason>` is `At data quality step '<step name>': <errorMessage>`.\n\n`approve` only works on entries quarantined by a source's manual-approval settings; against any other cause it returns HTTP 400. Excessive quarantine entries mean the match rules need work, not the runtime.\n\n**`delete` resolves, does not remove.** Returns `<mdm:success>true</mdm:success>` and drops from `type=\"ACTIVE\"`, but `type=\"ALL\"` still lists it as `<resolution>USER_IGNORE</resolution>`. A second `delete` → HTTP 404. Resolution is permanent.\n\n**Newer entries supersede older ones per `sourceEntityId`** (`<resolution>SUPERSEDED</resolution>`); only the newest stays ACTIVE.\n\n**`endDate` on a quarantine entry means \"resolved at\"**, not soft-deleted.\n\n## Size limits\n\nA model has two byte budgets, both deployment-blocking (not warnings) and both excluding the id field, reference fields, and repeatable (collection) fields:\n\n- **Total model size**: ~64 KB\n- **Single row size**: ~8 KB\n\nUTF8MB4 encoding makes STRING fields expensive (4 bytes/char), and each ENUMERATION field costs ~1 KB toward the model budget. Repeatable fields move to child tables, so they don't count toward either budget. Plan for these before publishing.\n\n## `<skill-path>` resolution\n\n- Take the absolute path of this SKILL.md and drop `/SKILL.md`. That is `<skill-path>`.\n- Verify by running `bash <skill-path>/scripts/datahub-env-check.sh` from a workspace with `.env`.\n- Treat `<skill-path>` as a fixed value for the session.\n\n## References\n\n```\nreferences/\n└── data_quality_step.md   # Use when: authoring, inspecting, or troubleshooting a model's data quality steps — business rule shape, quality-service and Integration process kinds, activation, ENRICH_ERROR triage\n```\n\n## Scripts inventory\n\nAll scripts support `--help` (or run with no args) for usage. They emit text (JSON or XML) to stdout and errors to stderr.\n\n**Pipeline discipline.** Pipe script output only to standard text filters (`head`, `tail`, `wc`, `grep`). Do **not** pipe to `python3 -c`, `jq -r`, `awk`, or other interpreters with inline code — some agent harnesses treat each piped executable as a separate trust boundary and prompt for approval per pipe, defeating the point of allowlisting the scripts. If you need a transformation an existing script doesn't provide, surface the request rather than work around it inline — it'll be added to the appropriate CLI tool.\n\n**Temp files stay in the project tree.** When you need a scratch file (XML query body, batch payload, etc.) to pass to a script, write it under `active-development/feedback/` (following bc-integration's working-files paradigm) — not `/tmp/`. The harness prompts for approval on writes outside the project tree, including `/tmp/`.\n\n**Use the Write tool, not bash heredocs.** Create the file with a separate **Write** tool call, then invoke the script with a separate **Bash** call. Do not combine `cat > file <<EOF ... EOF; bash <script> file` into one compound bash command — the harness's static analysis flags heredoc patterns (unquoted delimiters that allow shell expansion, multi-line compound blocks the parser can't fully reason about) and will prompt for approval. Two separate tool calls avoid the prompt entirely.\n\n- `scripts/datahub-common.sh` — sourced helper library.\n- `scripts/datahub-env-check.sh` — verify `.env` and reach the Platform API.\n- `scripts/datahub-model.sh` — `list | get | pull | quality-steps | add-quality-step | remove-quality-step | create | update | delete | publish`\n- `scripts/datahub-source.sh` — `list | get | pull | status | enable-initial-load | finish-initial-load | create | update | delete`\n- `scripts/datahub-repository.sh` — `list | get <repository-id> [--universe <id>] | status | clouds | create` (`--universe` scopes the response to one universe's summary within the repo)\n- `scripts/datahub-deployment.sh` — `deploy | undeploy | status | list`\n- `scripts/datahub-quarantine.sh` — `query | get | approve | reject | delete`\n- `scripts/datahub-golden-record.sh` — `query | query-enddated | get | history | meta | match | update | enddate | enddate-bulk | restore | unlink | get-by-source`\n- `scripts/datahub-connection.sh` — `bootstrap <rest|connector>` (creates a connection wired to this workspace's Data Hub creds, for use in integration processes). The two kinds are not interchangeable; `--help` covers which to pick and the `/mdm/` path prefix the `rest` kind needs.\n\nRepository API sub-commands take `--universe <id>` and read `DATAHUB_REPO_*` from `.env`.\n\n## Sample payloads\n\nThese are illustrative shapes. For existing artifacts (model, source) use `pull` to fetch the current XML and edit surgically; samples below are starting points for net-new.\n\n### `CreateModelRequest` — `datahub-model.sh create`\n\nFor updates, rename the wrapper to `<mdm:UpdateModelRequest>` (same inner content) and use `datahub-model.sh update <id>`.\n\n```xml\n<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<mdm:CreateModelRequest xmlns:mdm=\"http://mdm.api.platform.boomi.com/\">\n    <mdm:name>myModelName</mdm:name>\n    <mdm:fields>\n        <mdm:field name=\"recordId\"    repeatable=\"false\" required=\"true\"  type=\"STRING\" uniqueId=\"RECORDID\"   maxLength=\"50\"/>\n        <mdm:field name=\"recordValue\" repeatable=\"false\" required=\"false\" type=\"STRING\" uniqueId=\"RECORDVAL\"  maxLength=\"100\"/>\n    </mdm:fields>\n    <mdm:sources>\n        <mdm:source id=\"Manual\" type=\"Both\" allowMultipleLinks=\"false\" default=\"true\">\n            <mdm:inbound>\n                <mdm:createApproval required=\"false\"/>\n                <mdm:updateApproval required=\"false\"/>\n                <mdm:updateApprovalWithBaseValue>false</mdm:updateApprovalWithBaseValue>\n                <mdm:endDateApproval required=\"false\"/>\n                <mdm:earlyChangeDetectionEnabled>false</mdm:earlyChangeDetectionEnabled>\n            </mdm:inbound>\n            <mdm:outbound>\n                <mdm:channelUpdatesFields>All</mdm:channelUpdatesFields>\n                <mdm:sendCreates>true</mdm:sendCreates>\n            </mdm:outbound>\n        </mdm:source>\n    </mdm:sources>\n    <mdm:dataQualitySteps/>\n    <mdm:recordTitle>\n        <mdm:titleParameters>\n            <mdm:parameter uniqueId=\"RECORDID\"/>\n        </mdm:titleParameters>\n    </mdm:recordTitle>\n    <mdm:matchRules>\n        <mdm:matchRule topLevelOperator=\"AND\">\n            <mdm:simpleExpression>\n                <mdm:fieldUniqueId>RECORDID</mdm:fieldUniqueId>\n            </mdm:simpleExpression>\n        </mdm:matchRule>\n    </mdm:matchRules>\n    <mdm:tags/>\n</mdm:CreateModelRequest>\n```\n\n### `CreateSourceRequest` — `datahub-source.sh create`\n\nThree required: `<mdm:name>` (max 255 chars), `<mdm:sourceId>` (max 50 chars; `A-Z`, `a-z`, `0-9`, `_`, `-`), `<mdm:entityIdUrl>` (UI link template; use `{id}` placeholder, or empty string if none). For updates, rename wrapper to `<mdm:UpdateSourceRequest>`.\n\n```xml\n<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<mdm:CreateSourceRequest xmlns:mdm=\"http://mdm.api.platform.boomi.com/\">\n    <mdm:name>Salesforce Production</mdm:name>\n    <mdm:sourceId>SF</mdm:sourceId>\n    <mdm:entityIdUrl>https://example.my.salesforce.com/{id}</mdm:entityIdUrl>\n</mdm:CreateSourceRequest>\n```\n\n### `RecordQueryRequest` — `datahub-golden-record.sh query`\n\nDrop `<filter>` for \"return all (up to limit)\"; swap `EQUALS` for `CONTAINS` / `GREATER_THAN` / etc.; change `<filter op=\"AND\">` to `OR` for disjunction. Set `includeSourceLinks=\"true\"` on root for per-record source metadata.\n\nDo NOT include `xmlns=\"...\"` on `<RecordQueryRequest>` (HTTP 400 \"unable to read message body\"). For the first page, omit `offsetToken` entirely — `offsetToken=\"0\"` returns HTTP 400 \"unable to parse the provided offset token\". Use the token from the previous response's `<offsetToken>` for subsequent pages.\n\n```xml\n<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<RecordQueryRequest limit=\"10\">\n    <filter op=\"AND\">\n        <fieldValue>\n            <fieldId>RECORDID</fieldId>\n            <operator>EQUALS</operator>\n            <value>example-value</value>\n        </fieldValue>\n    </filter>\n</RecordQueryRequest>\n```\n\n### Batch upsert — `datahub-golden-record.sh update`\n\nThe `src` attribute is a configured contributing source. `<id>` is the source-side natural key — NOT the Data Hub record ID. Response is `202 Accepted` with `Location` ending in the batch ID. The source must already be update-capable: it completes the initial-load lifecycle (enable → upsert → finish; see § Model lifecycle) before its first upsert, or the batch returns HTTP 400 \"not yet marked as one that can send updates\".\n\n**Element naming rules:**\n- The entity wrapper element (e.g. `<contact>` below) must match the model's normalized `<mdm:name>` (see § Field types — lowercase, non-alphanumerics stripped). Wrong case fails synchronously with HTTP 400 \"entity of unknown type\".\n- Field elements use the field's `name` attribute (camelCase), NOT its `uniqueId` (UPPERCASE).\n- Field-group subfields must be wrapped in the group's element (e.g. `<address>...</address>`), not flat.\n- `<id>` values are NOT restricted to `A-Z, a-z, 0-9, _, -` ≤50 chars — that constraint applies to source `<mdm:sourceId>` at source-create time. Per-batch `<id>` values are preserved verbatim (`@`, length >50, etc. are accepted).\n\n**Failure modes — HTTP 202 does NOT mean success.**\n- HTTP 400 (synchronous): only on wrong entity-wrapper case.\n- HTTP 202 + silent quarantine: schema mismatches (wrong field name, missing group wrapper) land as `cause=PARSE_FAILURE`. Missing required fields land as `cause=REQUIRED_FIELD`. Always run `datahub-quarantine.sh query` after a batch to confirm records actually materialized as golden records.\n\n```xml\n<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<batch src=\"MANUAL\">\n    <contact>\n        <id>1</id>\n        <name>Bob Smith</name>\n        <city>Berwyn</city>\n    </contact>\n    <contact>\n        <id>2</id>\n        <name>Carol Jones</name>\n        <city>Boston</city>\n    </contact>\n</batch>\n```\n\n### `RecordEndDateRequest` — `datahub-golden-record.sh enddate-bulk`\n\n≤100 `<recordId>`, or a `<filter>` using `RecordQueryRequest`'s children — the two forms return differently, see § End-dating and restore. Single record: use `enddate <record-id>` (no body).\n\n```xml\n<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<RecordEndDateRequest>\n    <recordId>aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee</recordId>\n</RecordEndDateRequest>\n```\n\n### Source-driven end-date — `datahub-golden-record.sh update`\n\n`op=\"DELETE\"` on the entity root end-dates the linked golden record. `<id>` is the source-side entity id (`meta`'s `<link entityId=\"...\">`) and the only required element. HTTP 202. One-way — see § End-dating and restore.\n\n```xml\n<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<batch src=\"MANUAL\">\n    <contact op=\"DELETE\">\n        <id>1</id>\n    </contact>\n</batch>\n```\n\n### Match request — `datahub-golden-record.sh match`\n\nSame `<batch>` body shape as upsert; hits `POST /match` instead of `POST /records`. Preview-only — no writes. Per candidate, a hit returns the matched golden record as `<duplicate>` plus a `matchRule` attribute naming the rule logic that fired; a miss returns only the `<entity>`. Fuzzy hits include `<fuzzyMatchDetails>` with the computed `matchStrength` vs the configured `threshold` — use it to tune tolerance against real data.\n\n### `QuarantineQueryRequest` — `datahub-quarantine.sh query`\n\nRoot attributes (all optional): `limit`, `offsetToken`, `includeData` (default `true`), `type` (`ACTIVE` default | `RESOLVED` | `ALL`). Filter children: `<sourceId>`, `<sourceEntityId>`, `<createdDate>`, `<endDate>`, `<cause>`, `<resolution>`, `<field name=\"\" value=\"\"/>`. Empty `<QuarantineQueryRequest/>` returns all ACTIVE entries.\n\n```xml\n<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<QuarantineQueryRequest limit=\"50\" type=\"ACTIVE\">\n    <filter op=\"AND\">\n        <cause>POSSIBLE_DUPLICATE</cause>\n    </filter>\n</QuarantineQueryRequest>\n```\n"},"changes":[],"summary":"First saved snapshot. No earlier version is available for comparison.","summary_kind":"deterministic","summary_metadata":{}}