CRISalid Bus – Exchange Map & Message Catalog

This page documents the CRISalid message bus, with a global view of exchanges and queues, followed by detailed documentation for each exchange and its messages.


🌐 Global Map of Exchanges and Queues

CRISalid currently relies on several RabbitMQ exchanges to make its services communicate asynchronously.

In crisalid-bus, all exchanges are of type topic and use an “at-least-once” delivery strategy, which means that messages may be delivered more than once in case of failure, and consumers must be designed to handle potential duplicates and be idempotent. Consumers, in turn, must be able to handle the possibility of missing messages (e.g., by carefully processing “unchanged” events or implementing reconciliation mechanisms).

Messages are primarily of two types: events and tasks.

  • Events represent occurrences or changes in the system (e.g., “a person was created”, “a publication was harvested”). They are typically not acknowledged (i.e., they are fire-and-forget).
  • Tasks represent work that needs to be done (e.g., “harvest this publication”). They may be acknowledged (i.e., the producer expects a confirmation that the task was received and will be processed).

🧭 Exchange Overview Table

Exchange Purpose Type Delivery strategy Documented messages
directory Institutional directories and reference data topic at-least-once people, structures
publications Publication harvesting workflows topic at-least-once publication, harvestings
graph Downstream graph-facing integrations topic at-least-once people, structures,
publications, harvestings, changes

🔀 Routing key convention — interactive vs batch

All queues in the CRISalid bus will progressively be split into two queues suffixed -interactive and -batch. When a queue has been split, every message routed to it gains a 5th routing key segment: .interactive or .batch.

  • .interactive — message triggered by a real-time user action (e.g. a user requesting a document harvest, an operator editing a person record). Processing must be fast and must not be starved by bulk loads.
  • .batch — message produced by an ETL pipeline or mass-import job. Processing can tolerate higher latency.

Each split queue has its own channel and prefetch count, its own semaphore, and its own dead-letter queue on the exchange-specific dead-letter exchange (dlx.graph for graph-exchange queues, dlx.publications for publications-exchange queues, dlx.directory for directory-exchange queues) so failures in one traffic class do not affect the other.

Current status:

Service Split done? Queue names
sovisuplus ✅ sovisuplus-interactive, sovisuplus-batch
crisalid-ikg (inbound actions) ✅ crisalid-ikg-actions-interactive
crisalid-ikg (inbound directory) ✅ crisalid-ikg-people-batch, crisalid-ikg-structures-batch
crisalid-ikg (publications) ✅ crisalid-ikg-publications-batch, crisalid-ikg-publications-interactive, crisalid-ikg-harvesting-events-batch, crisalid-ikg-harvesting-events-interactive
svp-harvester ✅ svp-harvester-interactive, svp-harvester-batch

🗺️ Existing Queues

Queue Inventory

Queue Exchange Binding keys Producer(s) Consumer(s) Notes
crisalid-ikg-people-batch directory event.people.person.*.batch ETL pipelines crisalid-ikg DLQ on dlx.directory
dlq.crisalid-ikg-people-batch dlx.directory crisalid-ikg-people-batch dead-letter manual Dead-letter queue
crisalid-ikg-structures-batch directory event.structures.structure.*.batch ETL pipelines crisalid-ikg DLQ on dlx.directory
dlq.crisalid-ikg-structures-batch dlx.directory crisalid-ikg-structures-batch dead-letter manual Dead-letter queue
crisalid-ikg-actions-interactive graph task.*.*.*.interactive sovisuplus crisalid-ikg DLQ on dlx.graph
dlq.crisalid-ikg-actions-interactive dlx.graph crisalid-ikg-actions-interactive dead-letter manual Dead-letter queue
sovisuplus-interactive graph event.*.*.*.interactive crisalid-ikg sovisuplus DLQ on dlx.graph
sovisuplus-batch graph event.*.*.*.batch crisalid-ikg sovisuplus DLQ on dlx.graph
dlq.sovisuplus-interactive dlx.graph sovisuplus-interactive dead-letter manual Dead-letter queue
dlq.sovisuplus-batch dlx.graph sovisuplus-batch dead-letter manual Dead-letter queue
crisalid-ikg-publications-batch publications event.references.reference.*.batch svp-harvester crisalid-ikg DLQ on dlx.publications
dlq.crisalid-ikg-publications-batch dlx.publications crisalid-ikg-publications-batch dead-letter manual Dead-letter queue
crisalid-ikg-publications-interactive publications event.references.reference.*.interactive svp-harvester crisalid-ikg DLQ on dlx.publications
dlq.crisalid-ikg-publications-interactive dlx.publications crisalid-ikg-publications-interactive dead-letter manual Dead-letter queue
crisalid-ikg-harvesting-events-batch publications event.references.*.*.batch svp-harvester crisalid-ikg DLQ on dlx.publications
dlq.crisalid-ikg-harvesting-events-batch dlx.publications crisalid-ikg-harvesting-events-batch dead-letter manual Dead-letter queue
crisalid-ikg-harvesting-events-interactive publications event.references.*.*.interactive svp-harvester crisalid-ikg DLQ on dlx.publications
dlq.crisalid-ikg-harvesting-events-interactive dlx.publications crisalid-ikg-harvesting-events-interactive dead-letter manual Dead-letter queue
svp-harvester-interactive publications task.entity.references.retrieval.interactive crisalid-ikg svp-harvester DLQ on dlx.publications
svp-harvester-batch publications task.entity.references.retrieval.batch crisalid-ikg svp-harvester DLQ on dlx.publications
dlq.svp-harvester-interactive dlx.publications svp-harvester-interactive dead-letter manual Dead-letter queue
dlq.svp-harvester-batch dlx.publications svp-harvester-batch dead-letter manual Dead-letter queue
ctd-publications publications event.references.reference.*.* svp-harvester CTD

📚 Documentation by Exchange


Exchange : directory

Purpose

Messages from institutional directories and related structure / people synchronization workflows.

Exchange Type

topic

Delivery Strategy

at-least-once

Topic and Routing Key Patterns

There are two main patterns for routing keys in the directory exchange. All directory messages are ETL-sourced and carry the .batch mode segment:

event.structures.structure.<action>.batch
event.people.person.<action>.batch
Full list of routing keys for each entity type
event.structures.structure.created.batch
event.structures.structure.updated.batch
event.structures.structure.deleted.batch
event.structures.structure.unchanged.batch
event.people.person.created.batch
event.people.person.updated.batch
event.people.person.deleted.batch
event.people.person.unchanged.batch

Message Catalog for directory exchange


Structure events (directory exchange)

Routing keys

Pattern:

event.structures.structure.<action>.batch
Full list of routing keys for structure events
event.structures.structure.created.batch
event.structures.structure.updated.batch
event.structures.structure.deleted.batch
event.structures.structure.unchanged.batch
Ack required
no
Payload
JSON schema for structure events
{
  "structures_event": {
    "type": "created|updated|deleted|unchanged",
    "data": {
      "generic_type": "institution|institution_subdivision|unit|unit_subdivision|team",
      "type": "EPE|UNIV|UMR|UR|UAR|IRL|UFR|FAC|...",
      "local_types": [
        {
          "value": "<string>",
          "language": "<language code>"
        }
      ],
      "main_mission": "research|scientific_services|administrative_services|teaching",
      "secondary_missions": [
        "research|scientific_services|administrative_services|teaching"
      ],
      "long_labels": [
        {
          "value": "<string>",
          "language": "<language code>"
        }
      ],
      "short_labels": [
        {
          "value": "<string>",
          "language": "<language code>"
        }
      ],
      "descriptions": [
        {
          "value": "<string>",
          "language": "<language code>"
        }
      ],
      "hceres_research_areas": [
        "<string>"
      ],
      "erc_research_fields": [
        "<string>"
      ],
      "hal_collection": "<string|null>",
      "contacts": [
        {
          "type": "<string>",
          "format": "<string>",
          "value": {}
        }
      ],
      "identifiers": [
        {
          "type": "local|uai|nns|ror|wikidata|scopus",
          "value": "<string>"
        }
      ],
      "relationships": [
        {
          "type": "member_of",
          "subtype": "main_supervision|associated_supervision|participating_supervision|null",
          "target": "<string>",
          "start_date": "YYYY-MM-DD|null",
          "end_date": "YYYY-MM-DD|null"
        },
        {
          "type": "part_of",
          "target": "<string>",
          "start_date": "YYYY-MM-DD|null",
          "end_date": "YYYY-MM-DD|null"
        }
      ]
    }
  }
}
Example payloads
Example of a “created” event for a research unit
{
  "structures_event": {
    "type": "created",
    "data": {
      "generic_type": "unit",
      "type": "UMR",
      "local_types": [],
      "main_mission": "research",
      "secondary_missions": [],
      "long_labels": [
        {
          "value": "Example Research Unit",
          "language": "en"
        },
        {
          "value": "Unité de recherche exemple",
          "language": "fr"
        }
      ],
      "short_labels": [
        {
          "value": "ERU",
          "language": "en"
        }
      ],
      "descriptions": [
        {
          "value": "Example description in English.",
          "language": "en"
        },
        {
          "value": "Description d'exemple en français.",
          "language": "fr"
        }
      ],
      "hceres_research_areas": [
        "ST6"
      ],
      "erc_research_fields": [],
      "hal_collection": "https://hal.science/EXAMPLE_COLLECTION",
      "contacts": [
        {
          "type": "postal_address",
          "format": "structured_physical_address",
          "value": {
            "country": "France",
            "zip_code": "00000",
            "city": "Example City",
            "street": "1 Example Street"
          }
        },
        {
          "type": "electronical_address",
          "format": "website_address",
          "value": {
            "uri": "https://www.example.org/"
          }
        }
      ],
      "identifiers": [
        {
          "type": "local",
          "value": "LOCAL-001"
        },
        {
          "type": "nns",
          "value": "NNS-EXAMPLE"
        },
        {
          "type": "ror",
          "value": "ROR-EXAMPLE"
        }
      ],
      "relationships": [
        {
          "type": "member_of",
          "subtype": "main_supervision",
          "target": "uai-EXAMPLE",
          "start_date": "2000-01-01",
          "end_date": null
        }
      ]
    }
  }
}
Additional considerations
  • A research structure must have a generic type and at least one type or local type.
  • Relationships of type “member_of” can have a subtype only when characterizing the supervision of a research unit. Inclusion relationships (“part_of”) do not have subtypes.
  • The targets of relationships must be the tracking ids of structures already created in previous messages, except for external institutions that you do not manage: in this case, simply indicate “uai-1234” or “ror-ABCD” according to the type of identifier you have for the external institution.
  • For details on typologies, see the CRISalid Wiki: https://www.esup-portail.org/wiki/spaces/ESUPCRISalid/pages/1673035787/Import+des+donn%C3%A9es+structures+v2

Additional example: Institution subdivision with temporal relationships (Faculty)
Example of a “created” event for a faculty with temporal inclusion relationship
{
  "structures_event": {
    "type": "created",
    "data": {
      "generic_type": "institution_subdivision",
      "type": "FAC",
      "local_types": [],
      "long_labels": [
        {
          "value": "Example Faculty of Sciences",
          "language": "en"
        },
        {
          "value": "Faculté des Sciences Exemple",
          "language": "fr"
        }
      ],
      "short_labels": [
        {
          "value": "Faculty Sciences",
          "language": "en"
        },
        {
          "value": "Fac Sciences",
          "language": "fr"
        }
      ],
      "descriptions": [
        {
          "value": "Example Faculty dedicated to scientific education and research.",
          "language": "en"
        },
        {
          "value": "Faculté exemple dédiée à l'enseignement et la recherche scientifique.",
          "language": "fr"
        }
      ],
      "identifiers": [
        {
          "type": "local",
          "value": "FAC-EXAMPLE-001"
        }
      ],
      "relationships": [
        {
          "type": "part_of",
          "target": "local-EXAMPLE-UNIV-001",
          "start_date": "2010-01-01",
          "end_date": "2030-12-31"
        }
      ],
      "contacts": []
    }
  }
}

Note: This example demonstrates part_of relationship with both start_date and end_date defined, showing a bounded temporal relationship.

Additional example: Institution subdivision with open-ended temporal relationship (UFR)
Example of a “created” event for a UFR (Unité de Formation et Recherche) with temporal inclusion
{
  "structures_event": {
    "type": "created",
    "data": {
      "generic_type": "institution_subdivision",
      "type": "UFR",
      "local_types": [],
      "long_labels": [
        {
          "value": "Example School of Medicine",
          "language": "en"
        },
        {
          "value": "UFR de Médecine Exemple",
          "language": "fr"
        }
      ],
      "short_labels": [
        {
          "value": "Medicine School",
          "language": "en"
        },
        {
          "value": "UFR Médecine",
          "language": "fr"
        }
      ],
      "descriptions": [
        {
          "value": "Example School of Medicine providing medical education and research.",
          "language": "en"
        },
        {
          "value": "Unité de Formation et de Recherche en Médecine Exemple.",
          "language": "fr"
        }
      ],
      "identifiers": [
        {
          "type": "local",
          "value": "UFR-MEDICINE-001"
        }
      ],
      "relationships": [
        {
          "type": "part_of",
          "target": "local-FAC-EXAMPLE-001",
          "start_date": "2010-01-01",
          "end_date": null
        }
      ],
      "contacts": []
    }
  }
}

Note: This example shows end_date: null, indicating an open-ended relationship (still active).

Additional example: Research unit with both member_of and part_of relationships
Example of a “created” event for a research unit with multiple relationship types
{
  "structures_event": {
    "type": "created",
    "data": {
      "generic_type": "unit",
      "type": "UMR",
      "local_types": [],
      "main_mission": "research",
      "secondary_missions": [],
      "long_labels": [
        {
          "value": "Example Research Unit",
          "language": "en"
        },
        {
          "value": "Unité de Recherche Exemple",
          "language": "fr"
        }
      ],
      "short_labels": [
        {
          "value": "ERU",
          "language": "en"
        },
        {
          "value": "URE",
          "language": "fr"
        }
      ],
      "descriptions": [
        {
          "value": "Example research unit specializing in biomedical research.",
          "language": "en"
        },
        {
          "value": "Unité de recherche exemple spécialisée en recherche biomédicale.",
          "language": "fr"
        }
      ],
      "identifiers": [
        {
          "type": "local",
          "value": "UMR-EXAMPLE-001"
        },
        {
          "type": "nns",
          "value": "000012345"
        }
      ],
      "relationships": [
        {
          "type": "part_of",
          "target": "local-PLATFORM-EXAMPLE-001",
          "start_date": null,
          "end_date": null
        },
        {
          "type": "part_of",
          "target": "local-UFR-MEDICINE-001",
          "start_date": null,
          "end_date": null
        },
        {
          "type": "member_of",
          "subtype": "main_supervision",
          "target": "local-EXAMPLE-UNIV-001",
          "start_date": "2000-01-01",
          "end_date": null
        },
        {
          "type": "member_of",
          "subtype": "associated_supervision",
          "target": "ror-RESEARCH-ORG-EXAMPLE",
          "start_date": "2005-06-15",
          "end_date": null
        }
      ],
      "contacts": []
    }
  }
}

Note: This example demonstrates the difference between: - part_of: Structural inclusion relationships (without subtypes) - member_of: Supervision relationships (with subtypes like main_supervision or associated_supervision)

Additional example: Service unit with limited temporal relationship (USM)
Example of a “created” event for a service unit with bounded temporal relationship
{
  "structures_event": {
    "type": "created",
    "data": {
      "generic_type": "unit",
      "type": "USM",
      "local_types": [],
      "main_mission": "scientific_services",
      "secondary_missions": [],
      "long_labels": [
        {
          "value": "Example Service and Technology Unit",
          "language": "en"
        },
        {
          "value": "Unité de Service et Technologie Exemple",
          "language": "fr"
        }
      ],
      "short_labels": [
        {
          "value": "Service Unit",
          "language": "en"
        },
        {
          "value": "Unité Service",
          "language": "fr"
        }
      ],
      "descriptions": [
        {
          "value": "Example service unit providing technical support and services.",
          "language": "en"
        },
        {
          "value": "Unité de service exemple fournissant un support technique.",
          "language": "fr"
        }
      ],
      "identifiers": [
        {
          "type": "local",
          "value": "USM-EXAMPLE-001"
        }
      ],
      "relationships": [
        {
          "type": "part_of",
          "target": "FACILITY-EXAMPLE-001",
          "start_date": "2015-06-01",
          "end_date": "2025-12-31"
        }
      ],
      "contacts": []
    }
  }
}

Note: This example shows a service unit with a bounded temporal relationship (limited existence period).

Additional example: Team within a research unit
Example of a “created” event for a research team
{
  "structures_event": {
    "type": "created",
    "data": {
      "generic_type": "team",
      "type": "TEAM",
      "local_types": [],
      "long_labels": [
        {
          "value": "Example Research Team",
          "language": "en"
        },
        {
          "value": "Équipe de Recherche Exemple",
          "language": "fr"
        }
      ],
      "short_labels": [
        {
          "value": "Team Example",
          "language": "en"
        },
        {
          "value": "Équipe Ex",
          "language": "fr"
        }
      ],
      "descriptions": [
        {
          "value": "Example research team focused on biomedical research.",
          "language": "en"
        },
        {
          "value": "Équipe de recherche exemple axée sur la recherche biomédicale.",
          "language": "fr"
        }
      ],
      "identifiers": [
        {
          "type": "local",
          "value": "TEAM-EXAMPLE-001"
        }
      ],
      "relationships": [
        {
          "type": "part_of",
          "target": "UMR-EXAMPLE-001"
        }
      ],
      "contacts": []
    }
  }
}

Note: Teams are typically sub-units within research units and have part_of relationships without temporal boundaries.


People events (directory exchange)

Routing keys

Pattern:

event.people.person.<action>.batch
Full list of routing keys for people events
event.people.person.created.batch
event.people.person.updated.batch
event.people.person.deleted.batch
event.people.person.unchanged.batch
Ack required
no
Payload
JSON schema for people events
{
  "people_event": {
    "type": "created|updated|deleted|unchanged",
    "data": {
      "display_name": "<string>",
      "display_name_variants": [
        "<string>"
      ],
      "names": [
        {
          "first_names": [
            {
              "value": "<string>",
              "language": "<language code>"
            }
          ],
          "last_names": [
            {
              "value": "<string>",
              "language": "<language code>"
            }
          ],
          "other_names": [
            {
              "value": "<string>",
              "language": "<language code>"
            }
          ]
        }
      ],
      "identifiers": [
        {
          "type": "orcid|idref|idhals|idhali|local|scopus",
          "value": "<string>"
        }
      ],
      "memberships": [
        {
          "entity_uid": "<string>",
          "start_date": "YYYY-MM-DD|null",
          "end_date": "YYYY-MM-DD|null",
          "past": false,
          "future": false
        }
      ],
      "employments": [
        {
          "entity_uid": "<string>",
          "position": {
            "code": "<string>"
          },
          "start_date": "YYYY-MM-DD|null",
          "end_date": "YYYY-MM-DD|null",
          "past": false,
          "future": false
        }
      ],
      "external": false
    }
  }
}
Example payloads
Example of a “created” event for a researcher with ORCID, IdRef, membership and employment
{
  "people_event": {
    "type": "created",
    "data": {
      "names": [
        {
          "first_names": [
            {
              "value": "Marie",
              "language": "fr"
            }
          ],
          "last_names": [
            {
              "value": "Lefebvre",
              "language": "fr"
            }
          ]
        }
      ],
      "identifiers": [
        {
          "type": "local",
          "value": "mlefebvre@univ-example.fr"
        },
        {
          "type": "orcid",
          "value": "0000-0002-1234-5678"
        },
        {
          "type": "idref",
          "value": "123456789"
        },
        {
          "type": "idhals",
          "value": "marie-lefebvre"
        }
      ],
      "memberships": [
        {
          "entity_uid": "local-U123",
          "start_date": "2018-09-01",
          "end_date": null
        }
      ],
      "employments": [
        {
          "entity_uid": "uai-0751818J",
          "position": {
            "code": "MCF"
          },
          "start_date": "2018-09-01",
          "end_date": null
        }
      ]
    }
  }
}
Example of an “updated” event for a researcher with two names (birth and married) and HAL numeric identifier
{
  "people_event": {
    "type": "updated",
    "data": {
      "display_name": "Sophie Martin",
      "display_name_variants": [
        "Sophie Durand",
        "S. Martin"
      ],
      "names": [
        {
          "first_names": [
            {
              "value": "Sophie",
              "language": "fr"
            }
          ],
          "last_names": [
            {
              "value": "Martin",
              "language": "fr"
            }
          ]
        },
        {
          "first_names": [
            {
              "value": "Sophie",
              "language": "fr"
            }
          ],
          "last_names": [
            {
              "value": "Durand",
              "language": "fr"
            }
          ]
        }
      ],
      "identifiers": [
        {
          "type": "local",
          "value": "smartin@univ-example.fr"
        },
        {
          "type": "orcid",
          "value": "0000-0003-9876-5432"
        },
        {
          "type": "idhali",
          "value": "987654"
        }
      ],
      "memberships": [
        {
          "entity_uid": "local-U123",
          "start_date": "2015-01-01",
          "end_date": null
        }
      ],
      "employments": [
        {
          "entity_uid": "uai-0751818J",
          "position": {
            "code": "PR"
          },
          "start_date": "2015-01-01",
          "end_date": null
        }
      ]
    }
  }
}
Example of an “unchanged” event for a researcher with membership only (no employment)
{
  "people_event": {
    "type": "unchanged",
    "data": {
      "names": [
        {
          "first_names": [
            {
              "value": "Jeanne",
              "language": "fr"
            }
          ],
          "last_names": [
            {
              "value": "Dupont",
              "language": "fr"
            }
          ]
        }
      ],
      "identifiers": [
        {
          "type": "local",
          "value": "jdupont@univ-example.fr"
        },
        {
          "type": "idref",
          "value": "012345678"
        }
      ],
      "memberships": [
        {
          "entity_uid": "local-U123",
          "start_date": "2020-09-01",
          "end_date": null
        }
      ]
    }
  }
}
Example of a “deleted” event
{
  "people_event": {
    "type": "deleted",
    "data": {
      "names": [
        {
          "first_names": [
            {
              "value": "Paul",
              "language": "fr"
            }
          ],
          "last_names": [
            {
              "value": "Bernard",
              "language": "fr"
            }
          ]
        }
      ],
      "identifiers": [
        {
          "type": "local",
          "value": "pbernard@univ-example.fr"
        }
      ]
    }
  }
}
Additional considerations
  • Either display_name or at least one entry in names must be provided. If display_name is absent, it is computed automatically from the first available name.
  • display_name_variants is optional. Use it when the person is known under multiple forms of their name (e.g. birth name alongside married name, or initials).
  • A person may have multiple names entries — use one entry per distinct name form (birth name, married name, pseudonym).
  • Identifier types: local (institutional login or employee id), orcid, idref, idhals (HAL login), idhali (HAL numeric id), scopus.
  • memberships.entity_uid must reference a research structure already known to IKG, using either the local-<id> format or an external identifier prefix such as uai-<code>.
  • employments.entity_uid references an institution. The position.code must be a valid HCERES position code (e.g. PR, MCF, DR, CR, IR, Post-doc).
  • external: true marks people who are not managed by the institution (e.g. co-authors discovered through publication harvesting). External people carry no memberships or employments.

Exchange : publications

Purpose

Publication harvesting workflows. crisalid-ikg publishes retrieval task messages; svp-harvester consumes them and publishes harvesting progress, per-reference events, and retrieval status back onto the same exchange.

Exchange Type

topic

Delivery Strategy

at-least-once

Topic and Routing Key Patterns

All routing keys on the publications exchange have 5 segments, where the last segment is the queue mode (see Routing key convention — interactive vs batch):

Inbound (crisalid-ikg → svp-harvester):

task.entity.references.retrieval.<mode>

Outbound (svp-harvester → downstream):

event.references.reference.<action>.<mode>
event.references.harvesting.state.<mode>
event.references.retrieval.state.<mode>
event.references.retrieval.error.<mode>

Where <mode> is interactive or batch, and <action> is created, updated, deleted, or unchanged.

Full list of routing keys
task.entity.references.retrieval.interactive
task.entity.references.retrieval.batch

event.references.reference.created.interactive
event.references.reference.updated.interactive
event.references.reference.deleted.interactive
event.references.reference.unchanged.interactive
event.references.reference.created.batch
event.references.reference.updated.batch
event.references.reference.deleted.batch
event.references.reference.unchanged.batch

event.references.harvesting.state.interactive
event.references.harvesting.state.batch

event.references.retrieval.state.interactive
event.references.retrieval.state.batch

event.references.retrieval.error.interactive
event.references.retrieval.error.batch

Message Catalog for publications exchange


Inbound messages (crisalid-ikg → svp-harvester-interactive / svp-harvester-batch)

Routing key
task.entity.references.retrieval.<mode>
Ack required
yes
Payload
{
  "type": "person",
  "fields": {
    "name": "<string>",
    "identifiers": [
      { "type": "<identifier_type>", "value": "<string>" }
    ]
  },
  "harvesters": ["<harvester_name>"],
  "events": ["created", "updated", "deleted", "unchanged"],
  "identifiers_safe_mode": false,
  "nullify": [],
  "reply": false
}

type is always "person". harvesters restricts which harvesters run (omit or leave empty for all). reply: true triggers an event.references.retrieval.state acknowledgement message before harvesting begins.


Reference event messages (svp-harvester → downstream)

Published once per reference per harvesting when a reference is created, updated, deleted, or confirmed unchanged.

Routing key
event.references.reference.<action>.<mode>

Where <action> is created, updated, deleted, or unchanged.

Ack required
no
Payload
{
  "reference_event": {
    "type": "created|updated|deleted|unchanged",
    "reference": {
      "source_identifier": "<string>",
      "harvester": "<string>",
      "harvester_version": "<string>",
      "identifiers": [],
      "manifestations": [],
      "titles": [{ "value": "<string>", "language": "<string>" }],
      "subtitles": [],
      "abstracts": [],
      "subjects": [],
      "domains": [],
      "document_type": [],
      "contributions": [],
      "issue": null,
      "page": null,
      "book": null,
      "raw_issued": "<string|null>",
      "issued": "<ISO datetime|null>",
      "created": "<ISO datetime>",
      "custom_metadata": {},
      "version": 0
    },
    "enhanced": false
  },
  "entity": {
    "identifiers": [{ "type": "<string>", "value": "<string>" }],
    "name": "<string>"
  },
  "harvesting": {
    "identifier_used_type": "<string|null>",
    "identifier_used_value": "<string|null>"
  }
}

Harvesting state messages (svp-harvester → downstream)

Published for each individual harvester as it transitions through states (idle → running → completed/failed/skipped).

Routing key
event.references.harvesting.state.<mode>
Ack required
no
Payload
{
  "harvester": "<string>",
  "state": "idle|running|completed|failed|skipped",
  "identifier_used_type": "<string|null>",
  "identifier_used_value": "<string|null>",
  "error": [],
  "entity": {
    "identifiers": [{ "type": "<string>", "value": "<string>" }],
    "name": "<string>"
  }
}

Retrieval state messages (svp-harvester → downstream)

Published when a retrieval is registered in the database. Only emitted when reply: true in the inbound message.

Routing key
event.references.retrieval.state.<mode>
Ack required
no
Payload
{
  "harvestings": [
    {
      "harvester": "<string>",
      "state": "idle|running|completed|failed|skipped",
      "identifier_used_type": "<string|null>",
      "identifier_used_value": "<string|null>",
      "error": [],
      "reference_events": []
    }
  ],
  "entity": {
    "identifiers": [{ "type": "<string>", "value": "<string>" }],
    "name": "<string>"
  }
}

Retrieval error messages (svp-harvester → downstream)

Published when a retrieval cannot start — the inbound payload fails Pydantic validation or carries no bibliographic identifiers. No DB record is created for these errors.

Routing key
event.references.retrieval.error.<mode>
Ack required
no
Payload
{
  "type": "Retrieval",
  "parameters": {
    "type": "person",
    "fields": { "..." },
    "identifiers_safe_mode": false
  },
  "message": "<error description>"
}

Exchange : graph

Purpose

Bidirectional exchange between crisalid-ikg and downstream consumers (primarily sovisuplus).

  • crisalid-ikg → sovisuplus: lifecycle events for people, research units, documents, and harvesting state/results.
  • sovisuplus → crisalid-ikg: task messages representing user-initiated graph operations (merge documents, fetch publications, update a person, etc.).

Exchange Type

topic

Delivery Strategy

at-least-once

Topic and Routing Key Patterns

All routing keys on the graph exchange have 5 segments, where the last segment is the queue mode (see Routing key convention — interactive vs batch):

event.structures.structure.<action>.<mode>
event.people.person.<action>.<mode>
event.documents.document.<action>.<mode>
event.harvestings.<event_type>.<action>.<mode>
event.changes.change.<outcome>.interactive
task.documents.document.<action>.<mode>
task.people.person.<action>.<mode>
task.people.documents.fetch.<mode>

Where <mode> is interactive or batch. Change events (<outcome> = applied or failed) are only ever emitted in interactive mode — see Change events.

Full list of event routing keys
event.structures.structure.created.interactive
event.structures.structure.updated.interactive
event.structures.structure.deleted.interactive
event.structures.structure.unchanged.interactive
event.structures.structure.created.batch
event.structures.structure.updated.batch
event.structures.structure.deleted.batch
event.structures.structure.unchanged.batch

event.people.person.created.interactive
event.people.person.updated.interactive
event.people.person.deleted.interactive
event.people.person.unchanged.interactive
event.people.person.created.batch
event.people.person.updated.batch
event.people.person.deleted.batch
event.people.person.unchanged.batch

event.documents.document.created.interactive
event.documents.document.updated.interactive
event.documents.document.deleted.interactive
event.documents.document.unchanged.interactive
event.documents.document.created.batch
event.documents.document.updated.batch
event.documents.document.deleted.batch
event.documents.document.unchanged.batch

event.harvestings.harvesting_state_event.*.interactive
event.harvestings.harvesting_result_event.*.interactive
event.harvestings.harvesting_state_event.*.batch
event.harvestings.harvesting_result_event.*.batch

event.changes.change.applied.interactive
event.changes.change.failed.interactive
Full list of task routing keys (sovisuplus → crisalid-ikg)
task.documents.document.merge.interactive
task.documents.document.unmerge.interactive
task.documents.document.update.interactive
task.people.person.update.interactive
task.people.documents.fetch.interactive

Message Catalog for graph exchange


Structure events (graph exchange)

Routing keys

Pattern:

event.structures.structure.<action>.<mode>
Full list of routing keys for structure events
event.structures.structure.created.interactive
event.structures.structure.updated.interactive
event.structures.structure.deleted.interactive
event.structures.structure.unchanged.interactive
event.structures.structure.created.batch
event.structures.structure.updated.batch
event.structures.structure.deleted.batch
event.structures.structure.unchanged.batch
Ack required
no
Payload

The payload is a flat object with three top-level keys. The fields section contains all data available for the structure in the IKG graph. main_mission and secondary_missions are only present for unit type structures (research units, support units, administrative units).

JSON schema for structure events
{
  "type": "unit|institution|institution_subdivision|unit_subdivision|team",
  "event": "created|updated|deleted|unchanged",
  "fields": {
    "uid": "<string>",
    "national_type": "UMR|UAR|UR|IRL|UNIV|EPE|EPST|GE|COMUE|UFR|FAC|TEAM|THEME|null",
    "local_types": [
      {
        "value": "<string>",
        "language": "<language code>"
      }
    ],
    "identifiers": [
      {
        "type": "local|uai|nns|ror|wikidata|scopus",
        "value": "<string>"
      }
    ],
    "long_labels": [
      {
        "value": "<string>",
        "language": "<language code>"
      }
    ],
    "short_labels": [
      {
        "value": "<string>",
        "language": "<language code>"
      }
    ],
    "descriptions": [
      {
        "value": "<string>",
        "language": "<language code>"
      }
    ],
    "memberships": [
      {
        "target": "<uid of the supervising or member-of organisation>",
        "position": "main_supervision|associated_supervision|participating_supervision|null",
        "start_date": "YYYY-MM-DD|null",
        "end_date": "YYYY-MM-DD|null"
      }
    ],
    "parents": [
      {
        "target": "<uid of the structurally containing organisation>",
        "start_date": "YYYY-MM-DD|null",
        "end_date": "YYYY-MM-DD|null"
      }
    ],
    "addresses": [
      {
        "street": [{"value": "<string>", "language": "<language code>"}],
        "city": [{"value": "<string>", "language": "<language code>"}],
        "zip_code": [{"value": "<string>", "language": "<language code>"}],
        "country": [{"value": "<string>", "language": "<language code>"}]
      }
    ],
    "electronical_addresses": [
      {
        "uri": "<string>"
      }
    ],
    "main_mission": "research|scientific_services|administrative_services|teaching",
    "secondary_missions": [
      "research|scientific_services|administrative_services|teaching"
    ],
    "external": false
  }
}

main_mission and secondary_missions are only present when type is unit. external is false for structures sourced from the institutional directory, and true for structures auto-created by IKG from the national registry (e.g. a supervising institution referenced by uid but not directly sent by the directory).

Example payloads
Example of a “created” event for a research unit with supervisions
{
  "type": "unit",
  "event": "created",
  "fields": {
    "uid": "local-123456",
    "national_type": "UMR",
    "local_types": [],
    "identifiers": [
      {"type": "local", "value": "123456"},
      {"type": "nns", "value": "200012123S"},
      {"type": "ror", "value": "02feahw73"}
    ],
    "long_labels": [
      {"value": "Laboratoire de recherche en astrophysique", "language": "fr"},
      {"value": "Laboratory for Astrophysics Research", "language": "en"}
    ],
    "short_labels": [
      {"value": "LRA", "language": "fr"}
    ],
    "descriptions": [
      {"value": "Unité mixte de recherche en astrophysique sous tutelle de l'université et du CNRS.", "language": "fr"},
      {"value": "Joint research unit in astrophysics under joint supervision of the university and CNRS.", "language": "en"}
    ],
    "memberships": [
      {
        "target": "uai-0751818J",
        "position": "main_supervision",
        "start_date": "2000-01-01",
        "end_date": null
      },
      {
        "target": "uai-0757581P",
        "position": "associated_supervision",
        "start_date": "2000-01-01",
        "end_date": null
      }
    ],
    "parents": [
      {
        "target": "local-FAC-SCI-001",
        "start_date": "2000-01-01",
        "end_date": null
      }
    ],
    "addresses": [],
    "electronical_addresses": [],
    "main_mission": "research",
    "secondary_missions": [],
    "external": false
  }
}
Example of an “updated” event for an institution
{
  "type": "institution",
  "event": "updated",
  "fields": {
    "uid": "uai-0751818J",
    "national_type": "UNIV",
    "local_types": [],
    "identifiers": [
      {"type": "uai", "value": "0751818J"},
      {"type": "ror", "value": "05ykr0d48"}
    ],
    "long_labels": [
      {"value": "Université Étienne Dupond", "language": "fr"}
    ],
    "short_labels": [
      {"value": "UniED", "language": "fr"}
    ],
    "descriptions": [],
    "memberships": [],
    "parents": [],
    "addresses": [],
    "electronical_addresses": [],
    "external": false
  }
}
Example of a “deleted” event for an institution subdivision
{
  "type": "institution_subdivision",
  "event": "deleted",
  "fields": {
    "uid": "local-FAC-SCI-001",
    "national_type": "FAC",
    "local_types": [],
    "identifiers": [
      {"type": "local", "value": "FAC-SCI-001"}
    ],
    "long_labels": [
      {"value": "Faculté des sciences", "language": "fr"}
    ],
    "short_labels": [],
    "descriptions": [],
    "memberships": [],
    "parents": [
      {
        "target": "uai-0751818J",
        "start_date": "1995-01-01",
        "end_date": null
      }
    ],
    "addresses": [],
    "electronical_addresses": [],
    "external": false
  }
}
Example of an “unchanged” event for a team with axis membership
{
  "type": "team",
  "event": "unchanged",
  "fields": {
    "uid": "local-TEAM-ASTRO-001",
    "national_type": "TEAM",
    "local_types": [],
    "identifiers": [
      {"type": "local", "value": "TEAM-ASTRO-001"}
    ],
    "long_labels": [
      {"value": "Équipe astrophysique théorique", "language": "fr"},
      {"value": "Theoretical Astrophysics Team", "language": "en"}
    ],
    "short_labels": [],
    "descriptions": [],
    "memberships": [
      {
        "target": "local-AXIS-OBS-001",
        "position": null,
        "start_date": "2010-01-01",
        "end_date": null
      }
    ],
    "parents": [
      {
        "target": "local-123456",
        "start_date": "2010-01-01",
        "end_date": null
      }
    ],
    "addresses": [],
    "electronical_addresses": [],
    "external": false
  }
}
Additional considerations
  • The type field reflects the structure’s generic_type: unit (research and support units), institution, institution_subdivision, unit_subdivision, team.
  • main_mission and secondary_missions are only present when type is unit. They are absent for institutions, institution subdivisions, unit subdivisions, and teams.
  • memberships carries MEMBER_OF relationships (supervision chains for research units; org-level membership for teams and subdivisions). The position field is only set for supervision relationships (main_supervision, associated_supervision, participating_supervision).
  • parents carries PART_OF relationships (structural inclusion — e.g. a faculty inside a university, a team inside a research unit).
  • addresses and electronical_addresses are included in the schema for completeness but are currently empty in all events — contact data is not yet persisted in the graph.
  • The uid is the IKG-computed primary key (e.g. local-123456, uai-0751818J). It is stable for the lifetime of the structure and can be used as a join key across events.
  • external: false means the structure was sourced from the institution’s own directory. external: true means the structure was auto-created by IKG from the national registry because it appeared as a relationship target (e.g. a supervising institution referenced by a UAI or ROR identifier but never sent directly by the directory). Registry-created structures carry only a limited set of fields (labels, identifiers, address).
  • An unchanged event is published when the inbound directory message produced no changes in the graph. Consumers implementing reconciliation logic can use it to confirm that the structure is still active.
  • A deleted event signals that the structure has been removed from the graph. Consumers should treat subsequent references to its uid as invalid.

Queues: sovisuplus-interactive and sovisuplus-batch

Overview

sovisuplus subscribes to two queues on the graph exchange. Both queues are asserted by sovisuplus at startup — no manual pre-creation is required, but the dlx.graph dead-letter exchange must exist beforehand (it is declared in definitions.sample.json).

Queue Binding keys Prefetch Env var
sovisuplus-interactive event.*.*.*.interactive 5 AMQP_INTERACTIVE_QUEUE_NAME
sovisuplus-batch event.*.*.*.batch 20 AMQP_BATCH_QUEUE_NAME

Each queue has a corresponding dead-letter queue (dlq.sovisuplus-interactive, dlq.sovisuplus-batch) bound to dlx.graph with the source queue name as routing key (e.g. sovisuplus-interactive for dlq.sovisuplus-interactive). Failed messages (handler throws) are nacked without requeue and routed to the DLQ.

sovisuplus also publishes task messages to the graph exchange. These are consumed by crisalid-ikg-actions-interactive.

Inbound messages

All inbound messages share the same top-level envelope:

{
  "type": "<entity type>",
  "event": "<action>",
  "fields": { ... }
}

Person events

Routing key pattern: event.people.person.<action>.<mode>

{
  "type": "person",
  "event": "created|updated|deleted|unchanged",
  "fields": {
    "uid": "<string>",
    "identifiers": [
      {
        "type": "local|orcid|idref|idhals|idhali|scopus",
        "value": "<string>"
      }
    ],
    "display_name": "<string>",
    "first_name": "<string>",
    "last_name": "<string>",
    "external": true,
    "memberships": [
      {
        "entity_uid": "<string>",
        "research_unit": {
          "names": ["<string>"],
          "identifiers": [{ "type": "<string>", "value": "<string>" }]
        },
        "start_date": "<ISO date|null>",
        "end_date": "<ISO date|null>"
      }
    ]
  }
}

Research unit events

Routing key pattern: event.structures.structure.<action>.<mode>

The payload schema is identical to structure events documented in Structure events (graph exchange).

Document events

Routing key pattern: event.documents.document.<action>.<mode>

{
  "type": "document",
  "event": "created|updated|deleted|unchanged",
  "fields": {
    "uid": "<string>",
    "identifiers": [{ "type": "<string>", "value": "<string>" }],
    "titles": [{ "value": "<string>", "language": "<string>" }]
  }
}

Harvesting events

Routing key pattern: event.harvestings.<event_type>.<action>.<mode>

There are two subtypes:

Harvesting state event (event_type = harvesting_state_event):

{
  "type": "harvesting_state_event",
  "event": "started|running|completed|error",
  "fields": {
    "harvester": "<string>",
    "state": "running|completed|error",
    "error": ["<string>"],
    "entity": {
      "identifiers": [{ "type": "<string>", "value": "<string>" }],
      "name": "<string>"
    }
  }
}

Harvesting result event (event_type = harvesting_result_event):

{
  "type": "harvesting_result_event",
  "event": "created|updated|unchanged|deleted",
  "fields": {
    "reference_event": {
      "type": "created|updated|unchanged|deleted",
      "reference": {
        "source_identifier": "<string>",
        "harvester": "<string>",
        "harvester_version": "<string>",
        "identifiers": [{ "type": "<string>", "value": "<string>" }],
        "titles": [{ "value": "<string>", "language": "<string>" }],
        "subtitles": [{ "value": "<string>", "language": "<string>" }],
        "abstracts": [{ "value": "<string>", "language": "<string|null>" }],
        "document_type": [{ "uri": "<string>", "label": "<string>" }],
        "contributions": [
          {
            "rank": "<integer|null>",
            "role": "<string|null>",
            "contributor": {
              "name": "<string|null>",
              "first_name": "<string|null>",
              "last_name": "<string|null>",
              "identifiers": [{ "type": "<string>", "value": "<string>" }]
            }
          }
        ],
        "issued": "<ISO date|null>",
        "raw_issued": "<string|null>",
        "version": "<integer>"
      },
      "enhanced": true
    },
    "entity": {
      "identifiers": [{ "type": "<string>", "value": "<string>" }],
      "name": "<string>"
    }
  }
}

Change events (user-action outcome)

Routing key pattern: event.changes.change.<outcome>.interactive

event.changes.change.applied.interactive
event.changes.change.failed.interactive

Published by crisalid-ikg after processing a registered user action (a task.documents.document.* message persisted as a Change node). They close the feedback loop: sovisuplus can unfreeze the frozen data, notify the user, and display per-item warnings. Emitted only in interactive mode — batch replays of stored changes after a document recompute stay silent (the outcome is still recorded on the Change node in the graph).

  • applied — the action was applied. A non-empty warnings array signals a partial success: some items were skipped or degraded (e.g. a contributor that could not be resolved). On success sovisuplus receives this event and the usual event.documents.document.updated.interactive.
  • failed — the action was not applied; error_message carries the cause. Also emitted when the inbound task message cannot be validated into a Change (correlation fields are then copied from the raw payload) or when the target document does not exist. No document event follows a failure.
{
  "type": "change",
  "event": "applied|failed",
  "fields": {
    "uid": "sovisuplus:<id>",
    "id": "<uuid>",
    "application": "sovisuplus",
    "person_uid": "<uid of the user who performed the action>",
    "target_type": "DOCUMENT",
    "target_uid": "<string>",
    "path": "<string|null>",
    "action_type": "ADD|REMOVE|UPDATE|MERGE",
    "status": "applied|failed",
    "error_message": "<string|null>",
    "warnings": [
      {
        "code": "<string>",
        "message": "<string>",
        "context": { }
      }
    ],
    "timestamp": "<ISO datetime>"
  }
}

uid is <application>:<id> — since sovisuplus minted the id, it can correlate the outcome with its own pending action directly. timestamp is when the outcome was recorded (not the original action time).

Warning codes are open-ended; currently emitted by the contribution update:

code Meaning context fields
UNRESOLVABLE_PERSON contribution skipped, person could not be resolved display_name, identifiers
EXTERNAL_PERSON_CREATION_FAILED external person creation failed display_name, error
MISSING_DISPLAY_NAME external person without display name identifiers
INVALID_IDENTIFIER identifier with unknown type or value not matching the type pattern, dropped before any write type, value, display_name
CONTRIBUTION_NOT_CREATED contribution creation returned no id person_uid
AFFILIATION_CONFLICT conflict while resolving an affiliation to an authority organization source_organization_uid, error
AFFILIATION_WITHOUT_IDENTIFIER affiliation without usable identifier skipped affiliation
Example payloads
Example of an “applied” event with a partial-success warning
{
  "type": "change",
  "event": "applied",
  "fields": {
    "uid": "sovisuplus:9f1c2e7a-5b3d-4c8a-9e21-7a0f4d6b1c33",
    "id": "9f1c2e7a-5b3d-4c8a-9e21-7a0f4d6b1c33",
    "application": "sovisuplus",
    "person_uid": "local-jdupont",
    "target_type": "DOCUMENT",
    "target_uid": "ebf29bba-9bf5-4bb4-8a66-fa5d7deea2b3",
    "path": "contributions",
    "action_type": "UPDATE",
    "status": "applied",
    "error_message": null,
    "warnings": [
      {
        "code": "INVALID_IDENTIFIER",
        "message": "Invalid person identifier ignored",
        "context": {
          "type": "idhals",
          "value": "123456",
          "display_name": "Arnaud Bernard"
        }
      }
    ],
    "timestamp": "2026-06-27T05:17:59.114Z"
  }
}
Example of a “failed” event (safety guard: no submitted contribution could be applied)
{
  "type": "change",
  "event": "failed",
  "fields": {
    "uid": "sovisuplus:2b6d8f10-1a4c-4e7b-b3d2-5c9e0a7f4d21",
    "id": "2b6d8f10-1a4c-4e7b-b3d2-5c9e0a7f4d21",
    "application": "sovisuplus",
    "person_uid": "local-jdupont",
    "target_type": "DOCUMENT",
    "target_uid": "ebf29bba-9bf5-4bb4-8a66-fa5d7deea2b3",
    "path": "contributions",
    "action_type": "UPDATE",
    "status": "failed",
    "error_message": "None of the 2 submitted contributions could be applied to document ebf29bba-9bf5-4bb4-8a66-fa5d7deea2b3; aborting to avoid removing all contributors (MISSING_DISPLAY_NAME: Cannot create external person without a display name; ...)",
    "warnings": [],
    "timestamp": "2026-06-27T05:18:02.007Z"
  }
}

Outbound messages (sovisuplus → crisalid-ikg-actions-interactive)

sovisuplus publishes task messages to the graph exchange with routing keys of the form task.<domain>.<entity>.<action>.interactive. All outbound messages from sovisuplus carry the .interactive suffix — batch task publishing is not implemented.

The queue crisalid-ikg-actions-interactive is bound to receive all task.*.*.*.interactive keys.

{
  "uid": "sovisuplus:<id>",
  "id": "<uuid>",
  "actionType": "ADD|REMOVE|UPDATE|FETCH|MERGE",
  "targetType": "DOCUMENT|PERSON|HARVESTING",
  "targetUid": "<string>",
  "path": "<string|null>",
  "parameters": { ... },
  "timestamp": "<ISO datetime>",
  "personUid": "<string>",
  "application": "sovisuplus"
}

actionType and targetType are Prisma enum values serialized as uppercase strings. uid is the globally-unique message id (<application>:<id>) under which crisalid-ikg persists the action as a Change node; path selects the sub-resource the action targets (e.g. contributions, subjects).

actionType targetType path Routing key
MERGE DOCUMENT — task.documents.document.merge.interactive
REMOVE DOCUMENT — task.documents.document.remove.interactive
ADD DOCUMENT — task.documents.document.add.interactive
UPDATE DOCUMENT contributions task.documents.document.update.interactive
UPDATE PERSON — task.people.person.update.interactive
FETCH HARVESTING — task.people.documents.fetch.interactive

Contribution update task (UPDATE / DOCUMENT / path = contributions)

Routing key: task.documents.document.update.interactive

Emitted when a user saves the Authors tab of a document in sovisuplus. The message carries the complete, authoritative contributor list for the document. crisalid-ikg applies replace semantics: contributors present in the list are created/updated, contributors absent are removed, and an empty list removes all of them. The action is persisted as a Change and replayed after every recompute from source records, so a saved list pins the document against later harvests (newly-harvested co-authors do not reappear until the user saves again).

Payload

parameters.contributions is the full, ordered contributor list. Each entry carries a person, its roles (Library of Congress relator URIs), an optional rank, and its affiliations.

JSON schema for the contributions parameter
{
  "contributions": [
    {
      "rank": "<integer|null>",
      "roles": ["<LoC relator URI>"],
      "person": {
        "uid": "<string|null>",
        "displayName": "<string>",
        "firstName": "<string|null>",
        "lastName": "<string|null>",
        "identifiers": [
          { "type": "orcid|idref|idhals|idhali|scopus|local", "value": "<string>" }
        ]
      },
      "affiliations": [
        {
          "name": "<string|null>",
          "label": "<string|null>",
          "acronym": "<string|null>",
          "type": "institution|laboratory|researchteam|regroupinstitution|regrouplaboratory|regroupresearchteam",
          "hal": "<HAL structId|null>",
          "idref": "<string|null>",
          "ror": "<string|null>",
          "isni": "<string|null>",
          "nns": "<string|null>",
          "wikidata": "<string|null>"
        }
      ]
    }
  ]
}
Example payload
Example of a contribution-update message with an internal, an external and a brand-new contributor
{
  "uid": "sovisuplus:9f1c2e7a-5b3d-4c8a-9e21-7a0f4d6b1c33",
  "id": "9f1c2e7a-5b3d-4c8a-9e21-7a0f4d6b1c33",
  "actionType": "UPDATE",
  "targetType": "DOCUMENT",
  "targetUid": "ebf29bba-9bf5-4bb4-8a66-fa5d7deea2b3",
  "path": "contributions",
  "personUid": "local-jdupont",
  "application": "sovisuplus",
  "timestamp": "2026-06-27T05:17:57.842Z",
  "parameters": {
    "contributions": [
      {
        "rank": null,
        "roles": ["http://id.loc.gov/vocabulary/relators/aut"],
        "person": {
          "uid": "local-jdupont",
          "displayName": "Jeanne Dupont",
          "firstName": "Jeanne",
          "lastName": "Dupont",
          "identifiers": [{ "type": "idref", "value": "123456789" }]
        },
        "affiliations": [
          {
            "name": "Université Étienne Dupond",
            "label": null,
            "acronym": null,
            "type": "institution",
            "hal": null,
            "idref": "123456789",
            "ror": "05ykr0d48",
            "isni": "0000000121737435",
            "nns": null,
            "wikidata": "Q111111"
          }
        ]
      },
      {
        "rank": null,
        "roles": ["http://id.loc.gov/vocabulary/relators/ctb"],
        "person": {
          "uid": "orcid-0000-0002-7428-4208",
          "displayName": "Bernard Martin",
          "firstName": null,
          "lastName": null,
          "identifiers": [{ "type": "orcid", "value": "0000-0002-7428-4208" }]
        },
        "affiliations": [
          {
            "name": "Laboratoire Exemple",
            "label": "Laboratoire Exemple",
            "acronym": null,
            "type": "laboratory",
            "hal": "1009258",
            "idref": null,
            "ror": null,
            "isni": null,
            "nns": null,
            "wikidata": null
          }
        ]
      },
      {
        "rank": null,
        "roles": ["http://id.loc.gov/vocabulary/relators/ctb"],
        "person": {
          "uid": null,
          "displayName": "Arnaud Bernard",
          "firstName": "Arnaud",
          "lastName": "Bernard",
          "identifiers": []
        },
        "affiliations": []
      }
    ]
  }
}
Additional considerations
  • person.uid may be an internal uid (local-<id>), an external uid (orcid-…, idref-…, …), or null for a brand-new external contributor to be created from displayName. When a uid is given but not found, the person is matched by its identifiers, falling back to creation.
  • Internal persons (external: false): incoming identifiers and names are frozen (ignored); only the contribution itself and its affiliations are updated.
  • External persons (external: true): consistent incoming identifiers are added; identifiers that already belong to a different person are discarded.
  • affiliations are HAL-sourced (the sovisuplus author selector autocompletes against HAL), so each normally carries the HAL structId plus any of idref/ror/isni/nns/wikidata. They are resolved in memory to AuthorityOrganization nodes — no source-layer node is persisted.
  • roles are LoC relator URIs (…/aut author, …/ctb contributor, …/edt editor, …).
  • When several contribution-update changes accumulate on a document, only the latest is applied on replay; older ones are preserved for the forthcoming modification-history feature.
  • Identifiers with an unknown type or a value that does not match the type-specific pattern (e.g. an idhals made of digits) are dropped before any write and reported with an INVALID_IDENTIFIER warning on the outcome event.
  • Safety guard: if the submitted list is non-empty but none of its contributions can be applied, the change fails (previous contributors are kept) instead of wiping the contributor list. An intentionally empty list still removes all contributors.
  • The outcome (success, partial success with warnings, or failure) is reported back on event.changes.change.<applied|failed>.interactive — see Change events.