How to Invite an Actor to a Case
Use this guide to bring an actor into a case it was not present for. An invitation asks rather than asserts, so the actor joins only if it accepts. You finish with the actor either seated as a participant or recorded as having declined.
Prerequisites
- A running Vultron instance you can drive. The demo containers are the quickest route — see Tutorial: Run the Receive-Report Demo for the two steps that get you a container shell.
- An actor identity on that instance, holding the role each step names.
- Working knowledge of the ActivityStreams verb, object, and target shape, summarized in Vultron and ActivityPub.
-
Working knowledge of the case model: the objects a case is made of, in The Case Model, and who is allowed to change them, in The CASE_MANAGER and the Case Ledger. Where a guide's messages are case-scoped, they route through the CASE_MANAGER and their results appear on the case ledger.
-
An existing case, and the Case Owner role on it.
- The actor's Uniform Resource Identifier (URI).
- The CASE_MANAGER's actor URI. It sends the invitation and receives the reply.
The exchange
The sequence diagram below shows both outcomes.
The Case Owner asks the CASE_MANAGER for the invitation with one Offer.
Every later message is between the CASE_MANAGER and the invited actor.
---
title: Invite Actor to Case
---
sequenceDiagram
actor O as Case Owner
participant CA as CASE_MANAGER
actor A as Actor
activate O
O ->> CA: Offer(actor=CaseOwner, object=Actor, target=Case, suggestedRoles)
activate CA
CA ->>+ A: Invite(actor=CASE_MANAGER, object=Actor, target=CaseStub, attributedTo=CaseOwner)
note over CA: Records inert CaseParticipant (RM Received)
note over A: Consider invitation
alt Accept Invitation
A -->> CA: Accept(object=Invite)
note over CA: Participant becomes active (RM stays Received)<br/>then sends case, ledger replay and full-case Invite
else Reject Invitation
A -->> CA: Reject(object=Invite)
note over CA: Closes inert record (RM Closed); kept as history
end
deactivate A
deactivate CA
deactivate O
Send the invitation
-
Trigger the invitation as Case Owner. Your container sends your own
Offer(Actor)to the CASE_MANAGER, with the invited actor as itsobject, the case as itstarget, and the roles you offer insuggestedRoles(CM-17-007). With no roles given, the CASE_MANAGER assigns a non-empty default role list (CM-16-003). A stub Invite always names at least one role, and the CASE_MANAGER refuses to send one with none (CM-11-019).Ask the Case Manager to invite an actor
Asks the Case Manager to invite the actor named by the object to the case named by the target, with the roles in
suggestedRoles. For an actor with no record on the case, or an invitee that has not answered, the Case Manager answers with the Stub Invite to that actor, not with an offer to the Case Owner.- When… the Case Owner decides to invite an actor to the case
- Send:
Offer(Actor)[target=VulnerabilityCase] - Send to: the case manager
- Sent by: a participant
- Distinguished by: sender is the case owner
- Reference: Ask the Case Manager to invite an actor
Example activity
{ "@context": "https://www.w3.org/ns/activitystreams", "type": "Offer", "id": "urn:uuid:c561571e-1b26-478a-9401-f40980f62122", "name": "https://vultron.example/organizations/vendorco Offer Coordinator LLC to urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0", "context": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0", "to": "https://vultron.example/case-actors/vendorco-case-actor", "actor": "https://vultron.example/organizations/vendorco", "target": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0", "object": { "@context": "https://www.w3.org/ns/activitystreams", "type": "Organization", "id": "https://vultron.example/organizations/coordinator", "name": "Coordinator LLC", "published": "2026-10-23T20:17:06+00:00", "updated": "2026-10-23T20:17:06+00:00", "inbox": { "@context": "https://www.w3.org/ns/activitystreams", "type": "OrderedCollection", "id": "https://vultron.example/organizations/coordinator/inbox", "items": [] }, "outbox": { "@context": "https://www.w3.org/ns/activitystreams", "type": "OrderedCollection", "id": "https://vultron.example/organizations/coordinator/outbox", "items": [] } }, "suggestedRoles": [ "coordinator" ] } -
The CASE_MANAGER commits and sends
Invite(Actor)to the actor's inbox, with a case stub as itstargetrather than the case itself (CM-11-013, CM-17-001), with itself as the ActivityStreamsactorand your identity inattributedTo(PCR-08-007, PCR-08-008). The trigger's202response means only that yourOfferwas queued, so watch for theInviteon the CASE_MANAGER's ledger, not in the response.Invite actor to case
Sends a Stub Invite carrying the case stub and the embargo terms the invitee would agree to.
- When… the Case Manager invites an actor to join the case
- Send:
Invite(Actor)[target=VulnerabilityCaseStub] - Send to: a participant
- Sent by: the case manager
- Reference: Invite actor to case
Example activity
{ "@context": "https://www.w3.org/ns/activitystreams", "type": "Invite", "id": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/invitation/1", "name": "https://vultron.example/organizations/vendorco Invite Coordinator LLC to urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/stub", "context": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0", "content": "We're inviting you to participate in VendorCo Case #70930046.", "to": "https://vultron.example/organizations/coordinator", "actor": "https://vultron.example/organizations/vendorco", "target": { "@context": "https://certcc.github.io/Vultron/ns/context.jsonld", "type": "VulnerabilityCaseStub", "id": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/stub", "summary": "A memory-corruption vulnerability in widget — details shared after acceptance.", "caseId": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0" }, "object": { "@context": "https://www.w3.org/ns/activitystreams", "type": "Organization", "id": "https://vultron.example/organizations/coordinator", "name": "Coordinator LLC", "published": "2026-10-23T20:17:06+00:00", "updated": "2026-10-23T20:17:06+00:00", "inbox": { "@context": "https://www.w3.org/ns/activitystreams", "type": "OrderedCollection", "id": "https://vultron.example/organizations/coordinator/inbox", "items": [] }, "outbox": { "@context": "https://www.w3.org/ns/activitystreams", "type": "OrderedCollection", "id": "https://vultron.example/organizations/coordinator/outbox", "items": [] } } } -
Set the reply deadline on the activity's
end_time. When it is present that value settles precedence over the invitee's local policy window; when it is absent the policy window applies instead (CM-28-002, ADR-0065). Either way the effective deadline is clamped down to the embargo's ownend_time, so anend_timethat outlives the embargo does not buy the invitee extra time (EP-07-006).
The CASE_MANAGER is the sender, not the Case Owner
Putting the Case Owner in the actor field makes the invitation unrecognizable to a conformant peer, which expects case-management handshakes to come from the CASE_MANAGER.
Record who asked for the invitation in attributedTo instead.
Answer an invitation
Send your reply to the CASE_MANAGER, never to the Case Owner.
-
If you are joining the case, send
Accept(Invite(Actor))with theInviteactivity as itsobject.Accept invite to case
Accepts the Stub Invite. The actor joins the case at RM Received.
- When… an invited actor agrees to join the case
- Send:
Accept(Invite(Actor)[target=VulnerabilityCaseStub]) - Send to: the case manager
- Sent by: the invitee
- Reference: Accept invite to case
Example activity
{ "@context": "https://www.w3.org/ns/activitystreams", "type": "Accept", "id": "urn:uuid:9495c01d-0739-472e-affe-b1625ad9775c", "name": "https://vultron.example/organizations/coordinator Accept https://vultron.example/organizations/vendorco Invite Coordinator LLC to urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/stub", "inReplyTo": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/invitation/1", "content": "We're accepting your invitation to participate in VendorCo Case #70930046.", "to": "https://vultron.example/organizations/vendorco", "actor": "https://vultron.example/organizations/coordinator", "object": { "@context": "https://www.w3.org/ns/activitystreams", "type": "Invite", "id": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/invitation/1", "name": "https://vultron.example/organizations/vendorco Invite Coordinator LLC to urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/stub", "context": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0", "published": "2026-06-01T19:12:00+00:00", "updated": "2026-06-01T19:12:00+00:00", "content": "We're inviting you to participate in VendorCo Case #70930046.", "to": "https://vultron.example/organizations/coordinator", "actor": "https://vultron.example/organizations/vendorco", "target": { "@context": "https://certcc.github.io/Vultron/ns/context.jsonld", "type": "VulnerabilityCaseStub", "id": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/stub", "summary": "A memory-corruption vulnerability in widget — details shared after acceptance.", "caseId": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0" }, "object": { "@context": "https://www.w3.org/ns/activitystreams", "type": "Organization", "id": "https://vultron.example/organizations/coordinator", "name": "Coordinator LLC", "published": "2026-10-23T20:17:06+00:00", "updated": "2026-10-23T20:17:06+00:00", "inbox": { "@context": "https://www.w3.org/ns/activitystreams", "type": "OrderedCollection", "id": "https://vultron.example/organizations/coordinator/inbox", "items": [] }, "outbox": { "@context": "https://www.w3.org/ns/activitystreams", "type": "OrderedCollection", "id": "https://vultron.example/organizations/coordinator/outbox", "items": [] } } } } -
If you are not joining, send
Reject(Invite(Actor))with theInviteas itsobject.Reject invite to case
Rejects the Stub Invite, which closes the invitee's participant record.
- When… an invited actor declines to join the case
- Send:
Reject(Invite(Actor)[target=VulnerabilityCaseStub]) - Send to: the case manager
- Sent by: the invitee
- Reference: Reject invite to case
Example activity
{ "@context": "https://www.w3.org/ns/activitystreams", "type": "Reject", "id": "urn:uuid:4ed0846e-fb26-4e78-95aa-145dad4a1e5d", "name": "https://vultron.example/organizations/coordinator Reject https://vultron.example/organizations/vendorco Invite Coordinator LLC to urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/stub", "inReplyTo": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/invitation/1", "content": "Thanks for the invitation, but we're declining to participate in VendorCo Case #70930046.", "to": "https://vultron.example/organizations/vendorco", "actor": "https://vultron.example/organizations/coordinator", "object": { "@context": "https://www.w3.org/ns/activitystreams", "type": "Invite", "id": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/invitation/1", "name": "https://vultron.example/organizations/vendorco Invite Coordinator LLC to urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/stub", "context": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0", "published": "2026-06-01T19:12:00+00:00", "updated": "2026-06-01T19:12:00+00:00", "content": "We're inviting you to participate in VendorCo Case #70930046.", "to": "https://vultron.example/organizations/coordinator", "actor": "https://vultron.example/organizations/vendorco", "target": { "@context": "https://certcc.github.io/Vultron/ns/context.jsonld", "type": "VulnerabilityCaseStub", "id": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/stub", "summary": "A memory-corruption vulnerability in widget — details shared after acceptance.", "caseId": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0" }, "object": { "@context": "https://www.w3.org/ns/activitystreams", "type": "Organization", "id": "https://vultron.example/organizations/coordinator", "name": "Coordinator LLC", "published": "2026-10-23T20:17:06+00:00", "updated": "2026-10-23T20:17:06+00:00", "inbox": { "@context": "https://www.w3.org/ns/activitystreams", "type": "OrderedCollection", "id": "https://vultron.example/organizations/coordinator/inbox", "items": [] }, "outbox": { "@context": "https://www.w3.org/ns/activitystreams", "type": "OrderedCollection", "id": "https://vultron.example/organizations/coordinator/outbox", "items": [] } } } }
On acceptance, the CASE_MANAGER commits the reply to the ledger, seats you at Report Management (RM) state RM.RECEIVED, signs your embargo consent if an embargo is active, sends Announce(VulnerabilityCase) to seed your replica, and backfills the earlier ledger entries (CM-17-004).
Announce vulnerability case
Sends the full case to the newly admitted Participant once its acceptance clears the embargo consent checks.
- When… the Case Manager has admitted a new Participant to the case
- Send:
Announce(VulnerabilityCase) - Send to: a participant
- Sent by: the case manager
- Reference: Announce vulnerability case
Example activity
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Announce",
"id": "urn:uuid:e73f24db-c3b4-4cae-b880-920d28d04730",
"name": "https://vultron.example/organizations/vendorco Announce VendorCo Case #70930046",
"actor": "https://vultron.example/organizations/vendorco",
"object": {
"id": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0",
"type": "VulnerabilityCase",
"name": "VendorCo Case #70930046",
"published": "2026-10-23T20:17:07+00:00",
"updated": "2026-10-23T20:17:07+00:00",
"attributedTo": "https://vultron.example/organizations/vendorco",
"caseParticipants": [
{
"id": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/participants/finndervul",
"type": "CaseParticipant",
"name": "Finn der Vul",
"context": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0",
"published": "2026-10-23T20:17:07+00:00",
"updated": "2026-10-23T20:17:07+00:00",
"attributedTo": "https://vultron.example/users/finndervul",
"caseRoles": [
"reporter"
],
"participantStatuses": [
{
"id": "urn:uuid:92d8b666-d2e4-4431-9ae4-07826f5627e9",
"type": "ParticipantStatus",
"name": "START",
"context": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0",
"published": "2026-10-23T20:17:07+00:00",
"updated": "2026-10-23T20:17:07+00:00",
"attributedTo": "https://vultron.example/users/finndervul",
"rmState": "START",
"caseEngagement": true,
"cvdRole": [
"REPORTER"
],
"@context": "https://certcc.github.io/Vultron/ns/context.jsonld"
}
],
"embargoConsents": [],
"joined": true,
"@context": "https://certcc.github.io/Vultron/ns/context.jsonld"
},
{
"id": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/participants/vendorco",
"type": "CaseParticipant",
"name": "VendorCo",
"context": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0",
"published": "2026-10-23T20:17:07+00:00",
"updated": "2026-10-23T20:17:07+00:00",
"attributedTo": "https://vultron.example/organizations/vendorco",
"caseRoles": [
"vendor"
],
"participantStatuses": [
{
"id": "urn:uuid:910bbee1-16d1-41a6-bdff-0591c85e10a5",
"type": "ParticipantStatus",
"name": "RECEIVED Vf",
"context": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0",
"published": "2026-10-23T20:17:07+00:00",
"updated": "2026-10-23T20:17:07+00:00",
"attributedTo": "https://vultron.example/organizations/vendorco",
"rmState": "RECEIVED",
"vfState": "Vf",
"caseEngagement": true,
"cvdRole": [
"OBSERVER"
],
"@context": "https://certcc.github.io/Vultron/ns/context.jsonld"
}
],
"embargoConsents": [],
"joined": true,
"@context": "https://certcc.github.io/Vultron/ns/context.jsonld"
}
],
"actorParticipantIndex": {
"https://vultron.example/users/finndervul": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/participants/finndervul",
"https://vultron.example/organizations/vendorco": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/participants/vendorco"
},
"vulnerabilityReports": [
{
"id": "https://demo.vultron.local/VulnerabilityReport/e28f4965-7838-4644-b4a8-10604c7ee92f",
"type": "VulnerabilityReport",
"name": "FDR-8675309",
"published": "2026-10-23T20:17:06+00:00",
"updated": "2026-10-23T20:17:06+00:00",
"content": "I found a vulnerability!",
"attributedTo": "https://vultron.example/users/finndervul",
"@context": "https://certcc.github.io/Vultron/ns/context.jsonld"
}
],
"caseStatuses": [
{
"id": "urn:uuid:f6eb7e92-a7c7-4a65-962b-2d3d2c936d25",
"type": "CaseStatus",
"name": "NONE pxa",
"context": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0",
"published": "2026-10-23T20:17:07+00:00",
"updated": "2026-10-23T20:17:07+00:00",
"attributedTo": "https://vultron.example/organizations/vendorco",
"emState": "NONE",
"pxaState": "pxa",
"@context": "https://certcc.github.io/Vultron/ns/context.jsonld"
}
],
"notes": [],
"embargoRegister": [],
"pendingEmbargoProposalIndex": {},
"caseActivity": [],
"genesisHash": "61f38fb955f8b894dab64e9149c18c5c95eb77848ed54ea9fb554d912281add1",
"ledgerPosition": {
"logIndex": -1,
"entryHash": "61f38fb955f8b894dab64e9149c18c5c95eb77848ed54ea9fb554d912281add1"
},
"parentCases": [],
"childCases": [],
"siblingCases": [],
"activeParticipants": [
"urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/participants/finndervul",
"urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0/participants/vendorco"
],
"@context": "https://certcc.github.io/Vultron/ns/context.jsonld"
}
}
Expect RM.RECEIVED, not RM.ACCEPTED.
Accepting an invitation says you are willing to join the case; it does not say you have validated the report, which you have not yet seen in full (CM-11-001).
After the case and the ledger replay reach you, the CASE_MANAGER sends a second Invite, the full-case Invite, and you judge the case by answering it — see How to Manage a Case Roster.
Do not answer the original report Offer: you were never offered that report (CM-11-020).
Rule on the case afterwards — see How to Advance a Case Through Report Management.
A reply that arrives after the Invite's deadline is still processed.
The one exception is an Accept of a stub Invite that a newer one replaced, which the CASE_MANAGER refuses and answers by naming the replacement; a Reject of it is honored (CM-11-014, CM-11-016).
A replacement or re-invite names the Invite it replaces in inReplyTo (CM-11-015, CM-11-016).
A Reject or an Accept from an actor with no participant record is refused (CM-11-018, CM-11-021).
An Accept of a stub Invite from an actor that already joined the case, or was removed from it, is a duplicate: the CASE_MANAGER records and sends nothing (CM-17-012).
Choose invitation over seating
Use as:Invite for an actor that was absent when the case was created.
Seat the Case Owner and any already-known participants, such as the Reporter, inline on the as:Create for the case instead — see How to Initialize a Case.
If you are not the Case Owner but you know an actor belongs on the case, suggest it rather than inviting it: see How to Suggest an Actor for a Case.
Verify
| What you sent | What to confirm |
|---|---|
Invite(Actor) |
The invitee holds an Invite whose actor is the CASE_MANAGER. |
Accept(Invite(Actor)) |
The case roster holds you, and you have a local case replica. |
Reject(Invite(Actor)) |
The CASE_MANAGER's ledger records your Reject; your participant entry is closed (RM Closed) and kept as history, but you are not an active participant. |
See it end to end
Try it: vultron-demo invite-actor
vultron-demo invite-actor
Or with Docker Compose:
DEMO=invite-actor docker compose -f docker/docker-compose.yml run --rm demo
The scenario invites one coordinator that accepts and a second that rejects.
Further reading
- Case Management Messages — the wire format and a rendered example for each activity above
- Activity Vocabulary Design — why late arrivals are invited rather than added
- Case Initialization — the case lifecycle these invitations sit inside