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_nameor at least one entry innamesmust be provided. Ifdisplay_nameis absent, it is computed automatically from the first available name. display_name_variantsis 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
namesentries — 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_uidmust reference a research structure already known to IKG, using either thelocal-<id>format or an external identifier prefix such asuai-<code>.employments.entity_uidreferences an institution. Theposition.codemust be a valid HCERES position code (e.g.PR,MCF,DR,CR,IR,Post-doc).external: truemarks people who are not managed by the institution (e.g. co-authors discovered through publication harvesting). External people carry nomembershipsoremployments.
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_missionandsecondary_missionsare only present whentypeisunit.externalisfalsefor structures sourced from the institutional directory, andtruefor 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
typefield reflects the structure’sgeneric_type:unit(research and support units),institution,institution_subdivision,unit_subdivision,team. main_missionandsecondary_missionsare only present whentypeisunit. They are absent for institutions, institution subdivisions, unit subdivisions, and teams.membershipscarriesMEMBER_OFrelationships (supervision chains for research units; org-level membership for teams and subdivisions). Thepositionfield is only set for supervision relationships (main_supervision,associated_supervision,participating_supervision).parentscarriesPART_OFrelationships (structural inclusion — e.g. a faculty inside a university, a team inside a research unit).addressesandelectronical_addressesare included in the schema for completeness but are currently empty in all events — contact data is not yet persisted in the graph.- The
uidis 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: falsemeans the structure was sourced from the institution’s own directory.external: truemeans 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
unchangedevent 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
deletedevent signals that the structure has been removed from the graph. Consumers should treat subsequent references to itsuidas 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-emptywarningsarray 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 usualevent.documents.document.updated.interactive.failed— the action was not applied;error_messagecarries the cause. Also emitted when the inbound task message cannot be validated into aChange(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.uidmay be an internal uid (local-<id>), an external uid (orcid-…,idref-…, …), ornullfor a brand-new external contributor to be created fromdisplayName. When a uid is given but not found, the person is matched by its identifiers, falling back to creation.- Internal persons (
external: false): incomingidentifiersand names are frozen (ignored); only the contribution itself and its affiliations are updated. - External persons (
external: true): consistent incomingidentifiersare added; identifiers that already belong to a different person are discarded. affiliationsare HAL-sourced (the sovisuplus author selector autocompletes against HAL), so each normally carries the HALstructIdplus any ofidref/ror/isni/nns/wikidata. They are resolved in memory toAuthorityOrganizationnodes — no source-layer node is persisted.rolesare LoC relator URIs (…/autauthor,…/ctbcontributor,…/edteditor, …).- 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
idhalsmade of digits) are dropped before any write and reported with anINVALID_IDENTIFIERwarning 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.