sumsub/agent-skills

sumsub-create-aml-resolution-rules

Create, edit, reorder, delete, and publish Sumsub AML Resolution Rules (the AML Resolution Rule Chain) that auto-review AML screening hits.

Voir la source
Document Skill original

Rendu depuis le dépôt source en conservant titres, exemples, code, tableaux, liens et images.

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 (draft, published, archive, library, upsert, reorder, delete, publish); 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 aml_rules_api.py subcommand — you never call these paths by hand.)

MethodPathSubcommandWhen
GET/draftdraftRead the editable revision. Auto-creates a draft from the published revision (or empty) — never 404s for an enabled tenant. Carries notices[].
GET/publishedpublishedRead the live revision (404 if nothing published yet).
GET/archivearchive [marker]Revision history, marker-paginated. Rate limit 5/min — don't poll.
GET/librarylibraryReady-made rule templates, grouped by category.
POST/draft/rulesupsertUpsert one rule: omit id to create (appended at the end of the chain), include an existing id to replace that rule. Response = full draft with recomputed notices[].
PUT/draft/rules/orderreorderReorder: body {"ruleIds": [...]} must list every rule id exactly once.
DELETE/draft/rules/{id}delete <id>Remove one rule from the draft by id. Idempotent — deleting an id not in the draft still succeeds. See the consent policy below.
POST/draft/publishpublishGo live. Refused while any error-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 4xx); 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 manageAmlCases + manageClientSettings 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 sbx: 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 4xx). 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 `sumsub-api-auth` skill — read it if you hit 401 Invalid signature.

⚠️ 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. aml_rules_api.py enforces this — it rejects tokens that don't start with sbx:.
VarExample
SUMSUB_APP_TOKENsbx:... — sandbox AI App Token from the dashboard.
SUMSUB_SECRET_KEYThe paired secret shown once at token creation.
SUMSUB_BASEOptional. Defaults to https://api.sumsub.com.

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)

  1. Rules run top to bottom — order matters.
  2. Each rule is evaluated against every hit that is not yet reviewed. A hit that gets a matchStatus (from moderation or an earlier rule) is skipped by later rules.
  3. Effects are visible mid-run: tags and recomputed screening.answer / screening.tags / screening.matchStatuses from earlier rules are seen by later ones — this enables chaining (rule A tags match_tin, rule B reacts to screening.tags containsAny ["match_tin"]).
  4. Put specific, decisive rules (ID matches, previous-review carry-over) before broad clean-up rules — a broad false_positive 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:

  1. Tenant entitlement `WATCHLISTS` — all-or-nothing, tenant-wide. Verify it by invoking the `sumsub-check-permissions` skill. WATCHLISTS present → AML screening is available and on by default on every level. WATCHLISTS 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.
  2. Per-level opt-out `disableWatchlists` — only relevant once gate 1 passes. Screening runs on a level unless that level sets the top-level disableWatchlists: true (how a client carves out levels that shouldn't be screened). This is the `sumsub-create-level` skill's concern, not this one.
WATCHLISTS entitlementLevel disableWatchlistsScreening runs on that level?Chain acts on its hits?
absent(any)No — off tenant-wideNo
presentabsent / falseYes (default)Yes
presenttrueNo — level opted outNo

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 WATCHLISTS 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:

  1. AML rule — condition "the hit carries the pep risk label and the applicant is under 30", tags action ["youngPep"].
  2. Workflow edge — a condition testing that checks.personWatchlist.tags contains youngPep → 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 `sumsub-create-workflow` for the routing half.

Procedure

  1. Confirm AML screening is enabled for the tenant (see Precondition). Invoke the `sumsub-check-permissions` skill and verify WATCHLISTS 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.
  2. 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 403, stop and report (see Endpoints above). Review the existing rules: if a rule with the same intent already exists, offer to update it by `id` 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.
  3. 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 rule definition — strip the id, adjust, and continue. Check the template's vendors list against the AML vendor the tenant uses.
  4. Write the rule spec (below). Author the condition as the real Condition AST, directly — there is no expression mini-language (see Authoring conditions). Scope the rule to an entity type with an `input.entityType` criterion (default individual — see Authoring conditions). Verify every exp 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 unknownField error notices that block publishing.
  5. 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 condition AST through, assembles the review/tags actions, validates operators / the matchStatus enum / the emptyAction guard, and errors here (before any network call) on a typo or a malformed AST.
  6. Show the resolved payload to the user and get explicit confirmation before the first upsert. If the rule's tags 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.
  7. 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 notices[]. 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).
  8. Inspect `notices`. error notices (e.g. invalidTag, unknownField, unknownFunction) block publishing — fix the rule and re-upsert with its id. warning/info are advisory; surface them. See the notice key table in references/aml-resolution-rules-schema.md.
  9. 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.
  10. Delete rules — only per the consent policy below. ${CLAUDE_SKILL_DIR}/scripts/aml_rules_api.py delete <ruleId>.
  11. 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 4xx. That's expected: report it and direct the user to publish from the Sumsub web UI (see the Endpoints note).
  12. Report (lead with rule names, never bare ids):

Deleting rules — consent policy

The delete 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 id 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 name) 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 notices[] — 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 review and/or tags — neither yields an emptyAction error notice. Review fields take a scalar (the builder JSON-encodes it into a {lit}) or an explicit {"exp": "..."} / {"lit": "..."} to compute or supply the value directly.

Authoring conditions

A rule's condition 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 conditionRaw 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 `input.entityType` 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 sanctions 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\"]"} ] }
    ] }
  ]
}
  • `{exp: "<path>"}` — 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"}).
  • `{lit: "<json>"}` — a literal, supplied already JSON-encoded as a string (the form the API stores): "\"complyAdvantageCSOM\"", "2", "true", "[\"pep\", \"sanctions\"]". An un-encoded literal ({lit: "BRA"}) is rejected by the API, so the encoding is required.
  • `op` 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 op against its known set.
  • Set membership on list fields (match.riskLabels, screening.tags, match.complyAdvantage.matchTypes): contains / containsAny = has the value(s); containsAll = has all of; *`containsOnly` = has nothing but the listed values. To match "only label X" (e.g. adverse-media-only hits) use `containsOnly ["X"]` — 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).
  • `call` 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 not* operators (notIn, notContains, notContainsAny, ne, notEmpty, …) — do not set a negate flag; a top-level condition.negate is rejected (UI-unsupported), matching the workflow skill.
  • Dates: any Date field (input.dob, match.previousReview.modifiedAt, match.sources.latestEndDate) exposes .ageInDays / .ageInYears / .year etc. — e.g. {op: "lt", args: [{exp: "match.previousReview.modifiedAt.ageInYears"}, {lit: "2"}]}.

The full Condition/And/Criterion AST, literal-encoding rules, operator list, and built-in function signatures live in references/aml-resolution-rules-schema.md; valid exp 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

See also

du même dépôt

Autres Skills

Tous les Skills
sumsub
Communauté

sumsub-analyze-regulation

Analyze a regulation document (PDF or text) and produce a Sumsub configuration plan — mapping regulatory requirements to Sumsub entities (levels, questionnaires, PoA presets, TM rules, workflows, AML resolution rules). TRIGGER when the user provides a regulation PDF, legal act, or compliance requirement document and wants to know what to configure in Sumsub. Acts as the entry point before invoking sumsub-create-level, sumsub-create-questionnaire, sumsub-create-poa-preset, sumsub-create-workflow, sumsub-create-aml-resolution-rules, and other skills. SKIP for direct entity creation requests (no regulatory context) or Sumsub API calls.

installations
1
GitHub Stars
6
Mis à jour
3 sept.
sumsub
Communauté

sumsub-api-auth

Authenticate to the Sumsub API with an App Token + secret key (HMAC-SHA256 request signing). TRIGGER when the user asks to "call / sign / authenticate Sumsub API requests", debugs 401 Unauthorized / signature errors against api.sumsub.com, or needs a working request example with X-App-Token / X-App-Access-Sig / X-App-Access-Ts headers. SKIP only when a more specific skill in this repo (questionnaire/level/workflow/POA-preset/generic) already covers the user's actual task — those skills sign requests the same way and only need this one for auth deep dives.

installations
1
GitHub Stars
6
Mis à jour
3 sept.
sumsub
Communauté

sumsub-api-generic

Catch-all fallback for any Sumsub API task that does NOT match a more specific skill (e.g. create-sumsub-level, sumsub-create-questionnaire, sumsub-api-auth). TRIGGER when the user wants to call, inspect, or debug a Sumsub API endpoint not otherwise covered — fetching applicants, listing levels, reviewing AML hits, exporting data, generating SDK tokens, anything against api.sumsub.com. The procedure — locate the right endpoint in the OpenAPI schema, read its request/response shape, build the payload, sign with App Token, and validate. SKIP whenever a narrower Sumsub skill already covers the request.

installations
1
GitHub Stars
6
Mis à jour
3 sept.
sumsub
Communauté

sumsub-create-cross-check-preset

Create or update a Sumsub cross-check preset (name/address comparison rules between POI and POA documents) via POST/PATCH /resources/api/agent/crossCheckPresets and GET /resources/api/agent/crossCheckPresets/{id}. TRIGGER ONLY when the user EXPLICITLY asks to override how cross-checks compare names or addresses — e.g. "switch name match to strict", "allow fuzzy address match", "ignore middle-name mismatch", "create a custom cross-check preset". SKIP by default — Sumsub ships sensible defaults tuned for the best conversion and approval rate, so most clients should NOT create a custom preset. SKIP for level creation (the default cross-check preset auto-applies to every level), for general KYC config, or any request that doesn't specifically name "cross-check rules", "name comparison", or "address comparison".

installations
1
GitHub Stars
6
Mis à jour
3 sept.