Merging users
This page defines the operational procedure for executing user account merges on an OpenCTI platform. It is intended for platform administrators operating under maintenance conditions.
1. Overview & Guiding Principles
The User Merge capability reassigns data ownership, entity associations, collaborative links, and audit history from a source user onto a target user.
Guiding Principles
- Manual, Unitary Execution (1-by-1): Merges are performed one pair at a time through GraphQL mutations. Batching is controlled externally by the operator (e.g. iterating over a CSV list), allowing human inspection of dry-run reports before each write.
- Two-Pass Execution: Every merge runs an in-memory computation (
dryRun: true) producing a full report before any write occurs. In a real pass (dryRun: false), the engine verifies that the platform state has not drifted before committing any update. - Platform at Rest: The merge engine relies on the platform being at rest. Ingestions, connectors, background workers and the scheduled platform tasks must all be stopped: a single concurrent write aborts the pass and leaves it partially applied.
- Non-Destructive Merge, Gated Deletion: The merge operation itself disables the source account (
account_status: Expired) and closes its access. Deleting the source account is a separate, explicitly gated operation requiring full coverage and zero pending references. - Never Delete the Source Account via Settings → Users: Merging an account disables it and marks it with
merged_into. Deleting it through the standard web UI is rejected by the platform to avoid running cascades that destroy transferred dashboards and triggers. Source account deletion must be conducted exclusively through the dedicated, gateduserMergeDeleteSourcemutation. - Strict Idempotency: All merge handlers are strictly idempotent. If an execution is interrupted, re-running the merge on the same pair is a safe no-op on already applied data.
Warning
The platform must be at rest for the whole batch. A write that changes what the merge plans between its dry pass and its real pass (an ingestion, a worker, a scheduled task, a user action) makes the real pass refuse, with nothing written. A write on a document the real pass is rewriting aborts it half applied, and the merge has to be run again. Section 3.1 lists what to stop and how to check.
2. GraphQL API Reference & Parameter Specification
All queries and mutations require the BYPASS capability:
* @auth(for: [BYPASS])
2.1 Mutations
Mutation userMerge
Executes or previews the merge of a source user into a target user.
Arguments
| Argument | Type | Required | Default | Description |
|---|---|---|---|---|
sourceId |
ID! |
Yes | — | The internal ID or standard ID of the user account to merge away. |
targetId |
ID! |
Yes | — | The internal ID or standard ID of the user account receiving data and ownership. |
options |
UserMergeOptions |
No | { dryRun: true, rightsStrategy: STRICT, acknowledgeExposureChange: false } |
Execution flags controlling dry-run mode, RBAC rights strategy, and safety acknowledgments. |
Return Type: UserMergeResult
| Field | Type | Description |
|---|---|---|
id |
ID! |
Unique execution identifier (UUID) assigned to this merge run. |
source_id |
ID! |
Resolved internal ID of the source user. |
target_id |
ID! |
Resolved internal ID of the target user. |
dry_run |
Boolean! |
true if this was a simulation run; false if writes were committed. |
rights_strategy |
UserMergeRightsStrategy! |
The strategy applied (STRICT or UNION). |
status |
UserMergeStatus! |
Outcome status: RUNNING, SUCCESS, or FAILED. |
started_at |
DateTime! |
Timestamp when the merge run initiated. |
completed_at |
DateTime |
Timestamp when the merge run terminated. |
message |
String |
Error or diagnostic message if the execution failed or was blocked. |
report |
UserMergeExecutionReport |
Detailed breakdown of changes per handler, alerts, and register coverage. |
Sub-structure: UserMergeExecutionReport
merge_id: ID of the merge execution.total_updated: Count of documents written (always0during a dry-run).handlers: Array ofUserMergeHandlerOutcome:handler: Identifier of the handler (e.g.scalar-user-references,filter-user-references).updated: Number of documents updated by this specific handler.changes: Array of planned changes:register_row_id: Register row answered for.entity_type: Target entity type.count: Number of entities or documents impacted.exact:trueif the count is strictly exact,falseif estimated.detail: Human-readable explanation of the change.
alerts: Array ofUserMergeRightsAlert:register_row_id: Register row raising the alert.kind: Alert category (rights,marking,organization,exposure).message: Detailed explanation of the alert.blocking:trueif this alert aborts the real pass unlessacknowledgeExposureChange: trueis provided.
Mutation userMergeDeleteSource
Permanently deletes the source user account document from the platform after a successful merge.
Arguments
| Argument | Type | Required | Description |
|---|---|---|---|
sourceId |
ID! |
Yes | The internal ID of the merged-away source user account to delete. |
targetId |
ID! |
Yes | The internal ID of the target user that received the source's data. |
Return Type
- Returns
ID!(the ID of the deleted source user account).
Safety Behavior
- Re-checks Readiness: Internally calls
userMergeSourceDeletionReadiness. Ifallowedisfalse, throws aFunctionalErrorand aborts. - Cascades Suppressed: Does not use
userDelete, preventing accidental cascading deletions of Triggers, Dashboards, and Public Feeds that now belong to the target user. - Session Purge & Audit: Kills residual Redis sessions and publishes an audit log event (
deletes merged user <email>).
2.2 Queries
Query userMergeCoverage
Inspects the current completeness of the merge engine against the 100-row user reference register.
Arguments
| Argument | Type | Required | Description |
|---|---|---|---|
disposition |
UserMergeDisposition |
No | Optional filter to view only rows with a specific disposition (TRANSFER, INVALIDATE, CONDITIONAL, RETAIN, OUT_OF_SCOPE). If omitted, returns all 100 rows. |
Return Type: UserMergeCoverage
| Field | Type | Description |
|---|---|---|
total |
Int! |
Total number of rows in the register (100). |
covered_count |
Int! |
Number of rows claimed by registered handlers. |
uncovered_count |
Int! |
Total number of unclaimed rows (includes non-gating retained rows). |
gating_uncovered_count |
Int! |
Number of unclaimed rows that gate source deletion (TRANSFER and CONDITIONAL). Must be 0 to allow source deletion. |
is_complete |
Boolean! |
true when gating_uncovered_count === 0. |
rows |
[UserMergeCoverageRow!]! |
Detailed inventory of register rows (row_id, label, path, disposition, covered, handler). |
Query userMergeJournal
Retrieves execution logs recorded by handlers during merge runs.
Arguments
| Argument | Type | Required | Default | Description |
|---|---|---|---|---|
mergeId |
ID |
No | null |
Target merge execution ID. If omitted, returns recent entries across all merge runs. |
first |
Int |
No | 20 |
Maximum number of entries to return (minimum: 1, maximum: 200). |
Return Type: [UserMergeJournalEntry!]!
- Array of log entries containing
id,merge_id,source_id,target_id,handler,dry_run,status,started_at,completed_at,message.
Query userMergeSourceDeletionReadiness
Evaluates whether a source user account can be safely deleted.
query userMergeSourceDeletionReadiness(
$sourceId: ID!
$targetId: ID!
): UserMergeSourceDeletionReadiness!
Arguments
| Argument | Type | Required | Description |
|---|---|---|---|
sourceId |
ID! |
Yes | The internal ID of the source user account. |
targetId |
ID! |
Yes | The internal ID of the target user account. |
Return Type: UserMergeSourceDeletionReadiness
| Field | Type | Description |
|---|---|---|
allowed |
Boolean! |
true if and only if all three deletion preconditions are satisfied. |
coverage_complete |
Boolean! |
true if all gating register rows are claimed by handlers. |
pending_change_count |
Int! |
Number of changes a live dry-run on the pair still plans. Not a count of references: a moved right counts once removed from the source and once granted to the target. Must be 0. |
blockers |
[String!]! |
Array of human-readable explanations explaining why deletion is refused. Empty when allowed is true. |
2.3 Input Options & Enumerations
Input UserMergeOptions
input UserMergeOptions {
dryRun: Boolean = true
rightsStrategy: UserMergeRightsStrategy = STRICT
acknowledgeExposureChange: Boolean = false
}
dryRun(Boolean, default:true):true: Executes a simulation pass. Calculates all planned changes and alerts without modifying platform data; only the merge journal is written, in Redis.total_updatedin the report will be0.false: Executes the real merge. Recomputes and verifies the plan against current platform state, then commits all updates across platform indices, Redis, and internal caches.rightsStrategy(UserMergeRightsStrategy, default:STRICT):STRICT: The target user's RBAC rights (groups, roles, capabilities, markings) remain strictly unchanged. Source memberships and permissions are discarded.UNION: The target user inherits the source user's group memberships, organization affiliations, and capabilities.acknowledgeExposureChange(Boolean, default:false):- Must be explicitly passed as
truein a real pass (dryRun: false) if the dry-run reported a blocking alert (blocking: true). - Two situations raise a blocking alert, under either strategy: a public feed, TAXII collection or live stream owned by the source would serve data with the target's access, and the target gains markings, organizations, groups or capabilities the source did not have; or elements list the source as an authorized member and move to a target without the
KNOWLEDGE_KNUPDATE_KNMANAGEAUTHMEMBERScapability.
Enum UserMergeRightsStrategy
STRICT: Target permissions are kept intact; source permissions are dropped.UNION: Target permissions are expanded by unioning source groups and organizations.
Enum UserMergeStatus
RUNNING: Merge pass is currently executing.SUCCESS: Merge pass completed with zero fatal errors.FAILED: Merge pass aborted due to pre-condition failure, unacknowledged alert, or database error.
Enum UserMergeDisposition
TRANSFER: Reference must be re-pointed to the target user.INVALIDATE: Reference/access must be revoked, closed, or dropped.CONDITIONAL: Dependent on lifecycle status or rights strategy.RETAIN: Reference must remain untouched (historical record or own account identity).OUT_OF_SCOPE: Not managed by user merge.
3. Mandatory Preconditions
Before initiating a merge batch, verify and fulfill every requirement in this checklist.
3.1 Platform at Rest
The merge rewrites documents in place. Elasticsearch refuses an update whose document version moved since the pass started, and the merge is deliberately configured to abort on that conflict rather than overwrite a concurrent change. A single object written by anything else during the window is therefore enough to stop the pass — and, because a bulk rewrite is not transactional, to leave it partially applied. Re-running is safe, but only once the platform is genuinely idle.
"At rest" means no writes at all, from any source: ingestion, background workers, scheduled platform tasks, and interactive users.
Order of operations
Stop the producers before the consumers, so the queues drain instead of growing.
- Stop ingestion. Stop every connector and feed pushing data into the platform. Managed connectors and ingestion feeds can be paused from the platform itself; external connectors are stopped wherever they are deployed.
- Wait for the queues to drain. Let the running workers finish what is already queued.
- Stop the workers. Once the queues are empty, stop every worker process.
- Disable the scheduled platform tasks. See below — these write with no connector and no worker involved, and a platform restart is required for the change to take effect.
- Prevent interactive writes. Notify users and confirm no session is still active. Any UI action writes an entity and a history record.
Scheduled platform tasks
These run inside the platform process. Stopping connectors and workers does not stop them. Disable them in the configuration, then restart the platform.
| Configuration key | What it writes during the window |
|---|---|
RULE_ENGINE__ENABLED |
Inferred entities and relationships |
TASK_SCHEDULER__ENABLED |
Bulk operations queued from the interface |
SYNC_MANAGER__ENABLED |
Everything pulled from a remote platform |
INGESTION_MANAGER__ENABLED |
Everything pulled from TAXII, RSS, CSV and JSON feeds |
PLAYBOOK_MANAGER__ENABLED |
Whatever the playbooks create or patch |
RETENTION_MANAGER__ENABLED |
Deletions of expired elements |
GARBAGE_COLLECTION_MANAGER__ENABLED |
Purges of deleted objects |
EXPIRATION_SCHEDULER__ENABLED |
Revocation of expired indicators |
INDICATOR_DECAY_MANAGER__ENABLED |
Indicator score updates |
PIR_MANAGER__ENABLED |
PIR scoring updates |
FILE_INDEX_MANAGER__ENABLED |
Indexed file content |
NOTIFICATION_MANAGER__ENABLED |
Notifications, which carry user references |
PUBLISHER_MANAGER__ENABLED |
Notification deliveries and digests |
CONNECTOR_MANAGER__ENABLED |
Closes and deletes connector works, which carry the user that ran them |
IMPORT_CSV_BUILT_IN_CONNECTOR__ENABLED |
CSV imports still queued: works, draft workspaces and files for the user who asked |
DATA_SANITY_MANAGER__ENABLED |
Merges of duplicated entities when a sanity operation runs, rewriting their creators |
HUB_REGISTRATION_MANAGER__ENABLED |
The Settings document, hourly on a registered platform, and news feed items per user |
EXCLUSION_LIST_CACHE_BUILD_MANAGER__ENABLED and PLATFORM_USAGE_METRICS_MANAGER__ENABLED can stay
on: they only read Elasticsearch and write to Redis. CATALOG_MANAGER__ENABLED only writes when a new
connector catalog ships, then patches managed connectors: disable it too for a strictly frozen window.
Warning
Leave HISTORY_MANAGER__ENABLED and ACTIVITY_MANAGER__ENABLED on.
Both only ever append to opencti_history; neither updates an existing record, so neither can
conflict with the rewrites. And the activity manager is the sole writer of the audit trace the
merge itself emits (merges user <source> into user <target>). Disabling it would run the batch
with no record that it happened.
Records written during the window are not rewritten either: the history payload handler cuts at the start of the first real merge on the pair, so the traces the merge produces are left as they are instead of being erased by a later pass.
Verifying the platform is at rest
Check from the platform itself rather than from the infrastructure, so the answer reflects what the
platform actually sees. rabbitMQMetrics requires the MODULES capability.
query PlatformAtRest {
# Expect every count at "0": nothing queued, nothing being processed.
rabbitMQMetrics {
consumers # Connected workers. Expect "0" once the workers are stopped.
overview {
queue_totals {
messages
messages_ready
messages_unacknowledged
}
}
}
# A running connector refreshes updated_at on every heartbeat. Expect it to stop moving.
# `active` is not enough on its own: a connector killed without unregistering stays true.
connectors {
name
active
updated_at
}
# Expect completed: true on every entry, or an empty list.
backgroundTasks(first: 50) {
edges {
node {
id
completed
task_expected_number
task_processed_number
}
}
}
}
Run the query twice, a few minutes apart. Identical counts and unchanged updated_at values confirm
the platform is idle rather than merely slow.
3.2 Operator Account Safeguards
- Capability Requirement: The operator executing the GraphQL requests must hold the
BYPASScapability (Platform Administrator). - Identity Guard: The operator account must NOT be one of the source accounts being merged. An operator cannot merge themselves away without terminating their own session mid-batch.
3.3 Mandatory Snapshot & Backup
Danger
OpenCTI does not have an automated rollback mechanism. Merges write partial documents across live Elasticsearch indices. A snapshot is your only recovery path in the event of an unrecoverable operational mistake.
Create a restorable snapshot of:
1. Elasticsearch / OpenSearch cluster indices (all indices, including opencti_internal_objects, opencti_history, opencti_deleted_objects).
2. Redis key-value store.
Verify that the snapshot completed successfully before launching the first merge.
3.4 Reverse Proxy & Network Timeout Settings
The dry-run only counts: it returns in seconds, even for an account named in millions of documents. The real run rewrites every document naming the source, so its duration follows the activity of the account, not the size of the platform. Estimated on a platform of 550M documents:
| Source account | Documents rewritten | Real run |
|---|---|---|
| Ordinary user | up to a few hundred thousand | under a minute |
| Very active user | about 5M | 5 to 15 minutes |
- The real run is synchronous. A run longer than the reverse proxy read timeout (Nginx, AWS ALB, Cloudflare, often 60 seconds) answers a 504 while it keeps running server-side, and so does one longer than
app:request_timeout(20 minutes by default). Raising the proxy timeout (proxy_read_timeout) avoids the error, but do not rely on the HTTP response: follow the run in the journal (section 4.4), and apply section 6.1 on a timeout. - The
countvalues of the dry-run report give the order of magnitude in advance.
4. Per-User Merge Procedure
For each account pair (sourceId, targetId), execute the following 4-step loop.
4.1 Step 1: Execute Dry-Run
Run the userMerge mutation with dryRun: true:
mutation UserMergeDryRun($sourceId: ID!, $targetId: ID!) {
userMerge(
sourceId: $sourceId
targetId: $targetId
options: {
dryRun: true
rightsStrategy: STRICT
acknowledgeExposureChange: false
}
) {
id
status
source_id
target_id
rights_strategy
report {
total_updated
handlers {
handler
changes {
register_row_id
entity_type
count
exact
detail
}
alerts {
register_row_id
kind
message
blocking
}
}
}
}
}
4.2 Step 2: Review and Interpret the Report
Inspect the returned report.handlers:
- Verify Handlers & Counts:
source-deactivation: Count is1(or0if the source was already disabled and carryingmerged_into === targetId). Note that an account manually expired before the merge will still count as1becausemerged_intomust be written.scalar-user-references: Documents where the source wascreator_id,user_id, etc.connector.user-id(underscalar-user-references): connectors registered under the source account. A non-zero count means connectors that authenticate as the source, and stop working once its tokens are revoked: see Step 3 and section 7.filter-user-references: Number of saved filters, triggers, or feeds containing the source user UUID.blob-user-references: Dashboards, playbooks, and draft update patches rewritten.history-attribution&history-context-data-payload: Past events and audit logs being re-attributed. The payload handler rewrites the subject ids and the recorded changes of a record, which the platform shows and resolves into names; the raw payload (input,list_params,filters) is retained as recorded (history.context-data-raw-payload).operational-relations: Assignee/Participant links being re-pointed or deduplicated.residual-references: Runs last. Claims the register rows no handler acts on, each with the reason, and reads and writes nothing (updated: 0). The merge answers for the references the register records: a reference it does not record is not searched for, and calls for a new register row and handler.- Inspect RBAC Differences & Rights Strategy:
- STRICT (Default & Recommended): The target user retains strictly their own groups, roles, and markings. Source memberships are dropped.
- UNION: Source groups, organizations, and capabilities are added to the target. Use only when the target must inherit existing source clearances.
- Inspect Alerts (
blocking: truevsblocking: false): - Public Sharing Exposure Alerts: If the source owned a public feed, TAXII collection or live stream, an alert is raised. It is blocking when the target's access is wider than the source's, under
STRICTas well asUNION: an organization the target belongs to is enough. Ifblocking: true, you must passacknowledgeExposureChange: trueduring Step 3. - Restricted Members Alert: If the source has restricted members on objects but the target lacks authorization management rights, a blocking alert is raised.
- Textual Mentions / Corrupt Filters: Informational alerts indicating that an unparsable filter or a free-text search UUID was found and left untouched.
4.3 Step 3: Execute Real Merge
Execute the merge with dryRun: false. Leave acknowledgeExposureChange: false: if the dry-run reported a blocking alert, the engine refuses the real run and nothing is written. Set it to true only once that alert has been reviewed in Step 2 and the exposure change it describes is accepted.
mutation UserMergeApply($sourceId: ID!, $targetId: ID!) {
userMerge(
sourceId: $sourceId
targetId: $targetId
options: {
dryRun: false
rightsStrategy: STRICT
acknowledgeExposureChange: false
}
) {
id
status
message
completed_at
report {
total_updated
}
}
}
The real run revokes every API token of the source. A connector, feed or script that authenticates with one of them stops working: give it a token of the target, or better of a dedicated service account, before restarting it (see section 7).
4.4 Step 4: Verify Completion via the Journal
Verify the outcome recorded in the journal:
query UserMergeJournalCheck($mergeId: ID!) {
userMergeJournal(mergeId: $mergeId, first: 50) {
id
handler
dry_run
status
message
started_at
completed_at
}
}
Confirm that every entry with dry_run: false completed with status SUCCESS. The entries with dry_run: true are the internal dry pass the real run starts with. The source account is now Expired, its API tokens revoked, sessions terminated, and its data references transferred to the target.
5. Source Account Deletion Procedure
Deleting the source account is an irreversible operation and must only be executed after the merge has succeeded and all references have been cleared.
5.1 Check Deletion Readiness
Warning
Delete the source within 30 days of the merge. The deletion gate reads the start of the first real merge on the pair from the merge journal, which expires after 30 days. Past that, the merge's own traces count as pending and the gate refuses; the only way out is a new real run, which rewrites those traces.
Query the deletion gate before attempting deletion:
query UserMergeCheckReadiness($sourceId: ID!, $targetId: ID!) {
userMergeSourceDeletionReadiness(sourceId: $sourceId, targetId: $targetId) {
allowed
coverage_complete
pending_change_count
blockers
}
}
Gate Criteria
The deletion gate enforces three mandatory conditions:
1. coverage_complete === true: Every gating register row (transfer and conditional, 62 of the 100) is claimed by a handler — this is gating_uncovered_count === 0. covered_count is expected to stay below 100 and is not what the gate reads: invalidate, retain, and out-of-scope rows are outside the gate.
2. pending_change_count === 0: A live dry-run on the pair plans no change, so nothing still points to the source user.
3. merged_into === <targetId>: The source carries the mark a real merge into this target wrote on it.
If allowed is false, review blockers. Re-run the merge if changes are still planned.
Danger
Never delete a merged source account through Settings → Users.
The merge disables the source, it does not delete it, so the account remains listed with its ordinary delete button. That button runs four cascades — triggers and digests, workspaces, notifications, public dashboard sharing — which all select by a reference to the account being deleted. After a complete merge they find nothing. If the merge missed a reference, they delete the trigger or the dashboard that carries it, and those objects now belong to the target. A gap a re-run would repair becomes a permanent loss.
The platform refuses that button on any account carrying merged_into. Delete the source with
userMergeDeleteSource (section 5.2), and do not clear merged_into to work around the refusal.
5.2 Execute Permanent Deletion
Once allowed: true, permanently delete the source account:
mutation UserMergeDeleteSource($sourceId: ID!, $targetId: ID!) {
userMergeDeleteSource(sourceId: $sourceId, targetId: $targetId)
}
The mutation re-verifies readiness internally before deletion, deletes the User entity document, kills residual sessions, and emits an administrative audit log (deletes merged user <email>). It does not run the cascades, and it does not go through the ordinary deletion path, so the merged_into mark never has to be cleared.
6. Incident Response & Recovery Guide
6.1 Incident 1: HTTP 504 / Client Disconnection / Timeout
Warning
DO NOT RE-LAUNCH THE MERGE IMMEDIATELY. Neither Node.js nor Apollo cancels an in-flight mutation when a client disconnects. The merge is continuing server-side.
Procedure:
1. Retrieve the latest journal entries without specifying a mergeId:
query {
userMergeJournal(first: 50) {
merge_id
source_id
target_id
handler
dry_run
status
started_at
completed_at
message
}
}
merge_id whose source_id and target_id match your pair.
3. Poll userMergeJournal(mergeId: "<merge_id>") until the residual-references entry with dry_run: false has completed. A real run journals its internal dry pass too, and that pass ends with the same handler before anything is written.
4. If all handlers succeeded, the merge is complete. Do not re-run.
6.2 Incident 2: Node Crash or Restart Mid-Merge
If the backend server process crashed or was restarted while a merge was writing:
- Safety Assessment: Writes are not atomic, but every handler is idempotent: re-running completes a partially applied merge.
- Procedure:
- Inspect
userMergeJournalto find the last handler that completed. - Re-run the merge with the exact same parameters (
dryRun: false). - The handlers that previously wrote will cleanly find 0 documents left to update (
updated: 0), and the remaining handlers will complete.
6.3 Incident 3: Platform Divergence Error
Error Message: Platform state changed between the dry pass and the real pass, nothing was written
- Root Cause: The engine recomputes all handler plans immediately before the first write. If any document count or plan fingerprint diverges from the dry pass, the engine refuses to write to prevent inconsistent state.
- Typical Triggers: Ingestion workers were left running and modified entities; an active user logged in and created references.
- Resolution:
- Verify workers and connectors are stopped.
- Run a fresh dry-run (
dryRun: true). - Inspect the updated report and proceed with the real run.
6.4 Incident 4: Unacknowledged Blocking Alerts
Error Message: Merge blocked by unacknowledged alerts, nothing was written
- Root Cause: A handler detected a change of exposure or of rights management (see section 2.3): a public endpoint served with a wider access, or authorized members moving to a target that cannot manage them.
- Resolution: Review the dry-run alerts. If the security change is intended, pass
acknowledgeExposureChange: trueinoptions.
7. Closing the Batch & Restoring Traffic
Once all accounts in the batch have been processed:
- Post-Merge Verification Checks:
- Log in as the target user.
- Verify that the source user's dashboards, investigation workspaces, cases, and incidents are visible and editable.
- Check the Activity Log to confirm audit traces (
merges user <source> into user <target>). - Restore the platform: Reverse the steps of section 3.1, in the opposite
order — re-enable the scheduled platform tasks and restart the platform, start the workers, then
start the connectors and ingestion feeds last, so nothing is queued before there is a consumer for
it. Before starting a connector or a feed, check that it does not authenticate with a token of a
merged source account: those tokens are revoked, and the
connector.user-idcount of the dry-run named the connectors concerned. Re-run the verification query of that section: the worker count should be back to its nominal value. - Re-open Platform Traffic: Re-enable user access through the reverse proxy.
8. GraphQL Operations Cheatsheet
Check Register Coverage
query CheckCoverage {
userMergeCoverage {
total
covered_count
uncovered_count
gating_uncovered_count
is_complete
}
}
Full Merge Flow
# 1. Dry Run
mutation DryRun($src: ID!, $dst: ID!) {
userMerge(sourceId: $src, targetId: $dst, options: { dryRun: true, rightsStrategy: STRICT }) {
id
status
report { total_updated }
}
}
# 2. Real Run — set acknowledgeExposureChange: true only after reviewing a blocking alert of the dry-run
mutation ApplyMerge($src: ID!, $dst: ID!) {
userMerge(sourceId: $src, targetId: $dst, options: { dryRun: false, rightsStrategy: STRICT, acknowledgeExposureChange: false }) {
id
status
report { total_updated }
}
}
# 3. Check Deletion Readiness
query CheckReadiness($src: ID!, $dst: ID!) {
userMergeSourceDeletionReadiness(sourceId: $src, targetId: $dst) {
allowed
blockers
pending_change_count
}
}
# 4. Delete Source User
mutation DeleteSource($src: ID!, $dst: ID!) {
userMergeDeleteSource(sourceId: $src, targetId: $dst)
}