Sumsub — Create AML Resolution Rules
Builds AML resolution rules from a compact spec, upserts them into the tenant's draft AML rule chain via the agent API, and (optionally) publishes the draft to make it live. The chain (also called the AML Resolution Rule Chain) auto-reviews incoming AML screening hits: each rule, in order, is evaluated against every not-yet-reviewed hit, and on match sets the hit's review (match status / risk level / whitelisted / note) and/or tags the case.
Every network step goes through one signed client,
${CLAUDE_SKILL_DIR}/scripts/aml_rules_api.py
(
,
,
,
,
,
,
,
); rule payloads are assembled by
${CLAUDE_SKILL_DIR}/scripts/build_aml_rule.py
.
Endpoints
All under
/resources/api/agent/amlResolutionRules
. There is
one rule chain per tenant, versioned as
draft → published → archived
revisions. (Each row maps to an
subcommand — you never call these paths by hand.)
| Method | Path | Subcommand | When |
|---|
| | | Read the editable revision. Auto-creates a draft from the published revision (or empty) — never 404s for an enabled tenant. Carries . |
| | | Read the live revision (404 if nothing published yet). |
| | | Revision history, marker-paginated. Rate limit 5/min — don't poll. |
| | | Ready-made rule templates, grouped by category. |
| | | Upsert one rule: omit to create (appended at the end of the chain), include an existing to replace that rule. Response = full draft with recomputed . |
| | | Reorder: body must list every rule id exactly once. |
| | | Remove one rule from the draft by id. Idempotent — deleting an id not in the draft still succeeds. See the consent policy below. |
| | | Go live. Refused while any -level notice exists. Previous published revision auto-archives. |
Publishing via the API is for the integration phase. Once the integration is considered active, the API may
refuse to publish (a
); the chain must then be reviewed and published by a human from the Sumsub web UI. Treat a publish refusal as expected at that stage, not an error to work around — report it and point the user to the web UI.
Token requirements beyond the usual: these are agent endpoints, so the App Token must be
AI-purpose (created from the AI token page in the dashboard UI) and carry the
+
permissions. Symptoms of getting this wrong:
403 "This endpoint is not allowed for this type of token"
→ the token is a regular App Token, not an AI one. Ask the user to mint an AI token.
404 "API is not available"
on every endpoint → the AML Resolution Rules API is not enabled for this tenant. Stop immediately — tell the user to contact their CSM or Sumsub support; do not retry other endpoints.
⚠️ Publishing affects LIVE production
The AML Resolution Rule Chain is shared across live and sandbox — only the AML cases / applicants are isolated, not the chain itself. Publishing changes the chain that auto-reviews
real production AML screening hits (clearing false positives, confirming true positives, whitelisting) — a sandbox
token does
not make this safe. Everything up to publish is safe: reading the draft, upserting, reordering, and deleting rules all touch only the
draft, which reviews nothing until it's published.
So publish
only on explicit user request, and know it can be
refused by the backend: once the chain is active — too many applicants have already been processed / the integration is complete — the API rejects the publish (a
). That is expected, not an error to work around: report it and tell the user to publish from the Sumsub web UI (see the Endpoints note).
Auth — App Token + secret (sandbox only)
This skill talks to the public Sumsub API and signs each request per
the authentication reference.
The full how-it-works writeup lives in the
skill — read it if you hit
.
⚠️ Sandbox tokens only. Do
not accept or use a production App Token
here — these rules auto-review real AML hits. If the user offers a prod
token, refuse and ask them to generate a sandbox pair at
https://cockpit.sumsub.com/checkus/home?sbx=true (
Connect Sumsub to
your AI agent ->
Build & configure ->
Generate token — that page
mints the
AI token these endpoints require, see above). Token + secret
are shown once — copy both before closing the dialog.
enforces this — it rejects tokens
that don't start with
.
| Var | Example |
|---|
| — sandbox AI App Token from the dashboard. |
| The paired secret shown once at token creation. |
| Optional. Defaults to . |
If the user has already supplied credentials in conversation, reuse them;
otherwise ask once before running. Never echo the secret back.
How the chain executes (keep this in mind while designing rules)
- Rules run top to bottom — order matters.
- Each rule is evaluated against every hit that is not yet reviewed. A hit that gets a (from moderation or an earlier rule) is skipped by later rules.
- Effects are visible mid-run: tags and recomputed / / from earlier rules are seen by later ones — this enables chaining (rule A tags , rule B reacts to
screening.tags containsAny ["match_tin"]
).
- Put specific, decisive rules (ID matches, previous-review carry-over) before broad clean-up rules — a broad rule placed first steals hits from preciser rules below it.
Precondition: AML screening must actually be running
This chain acts only on hits produced by AML screening. If screening isn't running, the rules are inert — they evaluate against nothing. Two independent gates decide whether screening runs on a given applicant level:
- Tenant entitlement — all-or-nothing, tenant-wide. Verify it by invoking the skill. present → AML screening is available and on by default on every level. absent → AML screening is off for the entire tenant; no applicant-level setting can turn it on, and this rule chain will never see a hit.
- Per-level opt-out — only relevant once gate 1 passes. Screening runs on a level unless that level sets the top-level (how a client carves out levels that shouldn't be screened). This is the skill's concern, not this one.
| entitlement | Level | Screening runs on that level? | Chain acts on its hits? |
|---|
| absent | (any) | No — off tenant-wide | No |
| present | absent / | Yes (default) | Yes |
| present | | No — level opted out | No |
The chain is
one per tenant and acts on hits from
every level where screening runs — it is not scoped to a level. So confirm gate 1
before building rules (step 0). If
is missing, anything you write here stays dormant until the entitlement is enabled.
Tags bridge into applicant workflows
Rule tags are
common applicant tags — tenant-wide, not AML-only. They don't just chain within the AML run: they also surface in
applicant workflow conditions as
checks.personWatchlist.tags
(individuals) and
checks.companyWatchlist.tags
(companies). That makes a tag the hand-off point between the rule chain and a workflow: the rule chain flags a nuanced situation, the workflow routes on it. Example:
- AML rule — condition "the hit carries the risk label and the applicant is under 30", tags action .
- Workflow edge — a testing that
checks.personWatchlist.tags
contains → a manual-review node.
Both skills author the
same Condition AST (see below), so the two halves read alike. When the user's goal includes routing or actions downstream of screening, set the tag here and point them to
for the routing half.
Procedure
- Confirm AML screening is enabled for the tenant (see Precondition). Invoke the skill and verify is among the tenant's allowed entitlements. If it's absent, AML screening is off tenant-wide, so this rule chain will never fire: tell the user plainly (the entitlement is enabled by their CSM / Sumsub support), and get explicit confirmation before proceeding — do not silently build a chain that does nothing. If present, proceed.
- Read the draft:
${CLAUDE_SKILL_DIR}/scripts/aml_rules_api.py draft
. This is also the availability check — on 404 "API is not available"
or , stop and report (see Endpoints above). Review the existing rules: if a rule with the same intent already exists, offer to update it by instead of appending a duplicate — upsert has no name deduplication. Note which rule ids exist before you make changes — you'll need that to honor the delete consent policy.
- Check the library first:
${CLAUDE_SKILL_DIR}/scripts/aml_rules_api.py library
. If a ready-made template matches the user's intent, start from its definition — strip the , adjust, and continue. Check the template's list against the AML vendor the tenant uses.
- Write the rule spec (below). Author the as the real Condition AST, directly — there is no expression mini-language (see Authoring conditions). Scope the rule to an entity type with an criterion (default individual — see Authoring conditions). Verify every path against references/aml-rules-context-fields.md, and any enum-backed field's allowed values against references/aml-rules-enumerations.md — unknown paths produce error notices that block publishing.
- Assemble the rule payload with
${CLAUDE_SKILL_DIR}/scripts/build_aml_rule.py
— compact spec on stdin → full rule JSON on stdout. The builder is a thin assembler: it passes the AST through, assembles the review/tags actions, validates operators / the enum / the emptyAction guard, and errors here (before any network call) on a typo or a malformed AST.
- Show the resolved payload to the user and get explicit confirmation before the first upsert. If the rule's action introduces a tag that doesn't exist yet, call that out explicitly: upserting will create it as a tenant-wide applicant tag (visible in dashboard filters, workflows, everywhere) — confirm the name is one worth keeping (clear, concise, reusable) or reuse an existing tag instead.
- Upsert via
${CLAUDE_SKILL_DIR}/scripts/build_aml_rule.py < spec.json | ${CLAUDE_SKILL_DIR}/scripts/aml_rules_api.py upsert
→ response is the full draft with recomputed . Repeat steps 3–6 for each rule when creating several. Record the id the server assigns each rule you create — that's how you know it's yours (step 9).
- Inspect . notices (e.g. , , ) block publishing — fix the rule and re-upsert with its . / are advisory; surface them. See the notice key table in references/aml-resolution-rules-schema.md.
- Order the chain. New rules land at the end. If the evaluation order needs changing, pass the full permutation:
echo '{"ruleIds": [...]}' | ${CLAUDE_SKILL_DIR}/scripts/aml_rules_api.py reorder
.
- Delete rules — only per the consent policy below.
${CLAUDE_SKILL_DIR}/scripts/aml_rules_api.py delete <ruleId>
.
- Publish only on explicit request (see the "Publishing affects LIVE production" note):
${CLAUDE_SKILL_DIR}/scripts/aml_rules_api.py publish
. Publishing makes the chain live against real AML hits across live and sandbox — never publish without the user asking; until then the draft is dormant. The backend may refuse the publish once the chain is active (too many applicants already processed / integration complete) — a . That's expected: report it and direct the user to publish from the Sumsub web UI (see the Endpoints note).
- Report (lead with rule names, never bare ids):
- The rule chain in evaluation order (names), the draft , and a notices summary.
- Draft (editable) view: https://cockpit.sumsub.com/checkus/sdkIntegrations/globalSettings/amlScreening?revision=draft&tab=amlRules&sbx=true
- Published (live) view, after publishing: https://cockpit.sumsub.com/checkus/sdkIntegrations/globalSettings/amlScreening?revision=published&tab=amlRules&sbx=true
- Render both as clickable markdown links; targets the Sandbox workspace.
- End with a dedicated
Rule id (for future updates / reorder): <id>
line — the one place a raw id belongs in your output.
- On failure: surface Sumsub's / verbatim.
Deleting rules — consent policy
The
subcommand removes a rule from the draft permanently (it's archived with the revision, but gone from the editable chain). Apply this gate:
- A rule you created in this session — delete it freely when it was a mistake, the user asked you to replace it, or you're iterating on your own work. (Updating in place by is usually better than delete-then-recreate, because it preserves the rule's position; reach for delete when the rule should genuinely go away.)
- Any rule you did not create — a pre-existing rule, one from an earlier session, or one a human authored — delete only with explicit user consent for that specific rule. Name the rule (by its ) and confirm before deleting. Never delete someone else's rule as a side effect of "cleaning up" or "rebuilding" the chain.
- When unsure whether a rule is yours, treat it as not yours and ask.
Deleting is idempotent and returns the full draft with recomputed
— re-inspect them afterward (removing a rule can change which tags/fields the remaining rules still justify).
Rule spec format (JSON on stdin)
jsonc
{
"name": "Auto-clear hits with incompatible date of birth", // required
"description": "Optional free-form description",
"id": "existing-rule-id", // only when updating an existing rule
// condition — the real Sumsub Condition AST, authored directly (see below).
"condition": {
"or": [
{ "and": [
{ "op": "ne", "args": [ { "exp": "input.entityType" }, { "lit": "\"company\"" } ] }, // individual-only — scope every rule (see Authoring conditions)
{ "op": "eq", "args": [ { "exp": "screening.vendor" }, { "lit": "\"complyAdvantageCSOM\"" } ] },
{ "op": "eq", "args": [ { "exp": "match.dobs.hasData" }, { "lit": "true" } ] },
{ "op": "call", "args": [ { "exp": "dobsIncompatible" }, { "exp": "match.dobs" }, { "exp": "input.dob" }, { "lit": "\"weakContainment\"" } ] }
] }
]
},
// Review action — what to set on matched hits (every field optional):
"review": {
"matchStatus": "false_positive", // unknown | potential_match | false_positive | true_positive
"riskLevel": "LOW", // vendor-specific — omit when unsure
"whitelisted": true, // accept the risk: allows approval despite the hit
"note": "Cleared automatically: DoB mismatch"
// OR the whole-review form (replaces all per-field keys):
// "expression": "match.previousReview" // carry over the previous screening's decision
},
// Tags added to the AML case when the rule matches. These are common applicant
// tags (tenant-wide, usable everywhere — not AML-only). A tag that doesn't exist
// yet is created automatically on upsert: prefer reusing an existing tag, and
// name new ones clearly and concisely (e.g. "youngPep", not "rule3_tag"):
"tags": ["auto_cleared_dob"]
}
A rule needs
and/or
— neither yields an
error notice. Review fields take a scalar (the builder JSON-encodes it into a
) or an explicit
/
to compute or supply the value directly.
Authoring conditions
A rule's
is the
real Sumsub Condition AST, written directly — the
same shape and rules as workflow edge conditions, so the two skills stay aligned. There is
no expression mini-language (the old
condition: "screening.vendor = …"
string and
key were removed; the builder rejects both with a pointer here).
⚠️ Scope every rule to an entity type. Individual and company matches expose different fields and read differently, so a rule built for one can misfire on the other.
Each rule MUST carry an criterion — infer the target from the rule's intent (person name / DoB / PEP signals → individual; company name / KYB signals → company), and
default to individual when unsure:
- Individual (the usual case):
{ "op": "ne", "args": [ {"exp": "input.entityType"}, {"lit": "\"company\""} ] }
— entityType ≠ company.
- Company / organization:
{ "op": "eq", "args": [ {"exp": "input.entityType"}, {"lit": "\"company\""} ] }
.
Omit it
only for a deliberately entity-agnostic rule (rare — e.g. "whitelist every
hit from country X"). Most library templates already carry this criterion — keep it when you adapt one.
jsonc
{
"or": [ // OR of AND-groups; first matching branch wins
{ "and": [
{ "op": "eq", "args": [ {"exp": "screening.vendor"}, {"lit": "\"complyAdvantageCSOM\""} ] },
{ "op": "in", "args": [ {"exp": "input.country"}, {"lit": "[\"BRA\", \"ARG\"]"} ] }
] }
]
}
- — an expression path, verbatim (from references/aml-rules-context-fields.md). It's the left side of a comparison, and also the right side for field-to-field checks (
{exp: "match.analysis.parentName"}
).
- — a literal, supplied already JSON-encoded as a string (the form the API stores):
"\"complyAdvantageCSOM\""
, , , "[\"pep\", \"sanctions\"]"
. An un-encoded literal () is rejected by the API, so the encoding is required.
- is one of the comparison operators (
eq ne lt lte gt gte in notIn contains notContains containsAny notContainsAny containsAll containsOnly startsWith endsWith match empty notEmpty eqIgnoreCase …
; full set in references/aml-resolution-rules-schema.md). The builder validates every against its known set.
- Set membership on list fields (, ,
match.complyAdvantage.matchTypes
): / = has the value(s); = has all of; = has nothing but the listed values. To match "only label X" (e.g. adverse-media-only hits) use — never notContainsAny [the-other-labels]
, which leaks because it silently assumes you enumerated the whole closed vocabulary (the risk-label set is exactly six — see references/aml-rules-enumerations.md).
- invokes a built-in function — the one operator AML conditions have that workflows don't. The first arg is the function name as
{exp: "namesIncompatible"}
, the rest are its arguments: {op: "call", args: [{exp: "dobsIncompatible"}, {exp: "match.dobs"}, {exp: "input.dob"}, {lit: "\"strict\""}]}
. Functions and their signatures: references/aml-resolution-rules-schema.md.
- Negation is expressed with the operators (, , , , , …) — do not set a flag; a top-level is rejected (UI-unsupported), matching the workflow skill.
- Dates: any field (,
match.previousReview.modifiedAt
, match.sources.latestEndDate
) exposes / / etc. — e.g. {op: "lt", args: [{exp: "match.previousReview.modifiedAt.ageInYears"}, {lit: "2"}]}
.
The full
/
/
AST, literal-encoding rules, operator list, and built-in function signatures live in
references/aml-resolution-rules-schema.md; valid
paths are in
references/aml-rules-context-fields.md and enum-field values in
references/aml-rules-enumerations.md (both indexed by
references/aml-rules-expressions.md).
Worked examples
- — tag-only rule ( on risk labels).
examples/auto-clear-dob-mismatch.json
— functions + vendor match types; the most common auto-clear topology.
examples/reuse-previous-review.json
— whole-review form carrying over a previous decision, with a date-age condition. Also the rare entity-agnostic rule (carrying a prior decision reads the same for individuals and companies) — the one case that needs no filter.
See also
- references/aml-resolution-rules-schema.md — endpoints with rate limits, every field and enum, condition AST shape, operator list, built-in function signatures, notice message keys, execution model.
- references/aml-rules-expressions.md — expression-reference index + preamble (root variables, / shapes, the enum caveat). Points to the two data files below; all three are lookup databases, don't read end-to-end.
- references/aml-rules-context-fields.md — every legal path with its type ( / / ).
- references/aml-rules-enumerations.md — allowed values for the closed-enum fields (risk labels, match statuses, PEP institution kinds, match types, …).
- — authors the same Condition AST in workflow edge conditions; its expressions reference covers the / shapes.