Skip to content
How-to guide

How to Delegate a Role to Another Participant

Use this guide to offer a CVDRole on a case to another participant. Delegation is an offer, so the participant offered the role decides whether to take it. The offer and the answer both travel through the case's Case Manager, which checks the offer, records it in the case ledger and forwards it. You finish with the participant holding the role, or with the roster unchanged.


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 on which you are the Case Owner. Only the Case Owner may offer a role, and the Case Manager refuses an offer from anyone else (CM-33-002).

  • The target participant's Uniform Resource Identifier (URI). The target must already be a participant in the case.
  • The specific CVDRole you intend to offer. You cannot offer CASE_OWNER or CASE_MANAGER, and you cannot offer a role the target already holds (CM-33-003). Ownership moves through an ownership transfer instead.

The exchange

The sequence diagram below shows both outcomes. The Case Owner sends the offer to the Case Manager, never to the target. The Case Manager records it and forwards it to the target, and the target answers the Case Manager.

---
title: Role Delegation Through the Case Manager
---
sequenceDiagram
    actor O as Case Owner
    participant M as Case Manager
    actor T as Target Participant
    O ->> M: Offer(CaseParticipantRole, target=T, context=Case)
    note over M: Check sender, role and target<br/>Record the offer in the ledger
    M ->> T: Offer(CaseParticipantRole), inReplyTo=the recorded offer
    note over T: Consider role offer
    alt Accept
        T -->> M: Accept(Offer(CaseParticipantRole))
        note over M: Check the sender is the target of an open offer<br/>Record the acceptance in the ledger<br/>Grant the role on its own copy
    else Reject
        T -->> M: Reject(Offer(CaseParticipantRole))
        note over M: Check the sender is the target of an open offer<br/>Record the refusal in the ledger<br/>Roles unchanged
    end
    M -->> O: Announce(CaseLedgerEntry)
    M -->> T: Announce(CaseLedgerEntry)
    note over O,T: Replay the entry<br/>On an Accept, the target now holds the role

Offer the role

  1. Send Offer(CaseParticipantRole) to the case's Case Manager only, with the Case Manager as the sole to recipient (CM-33-001).
  2. Carry the role in a CaseParticipantRole object, name the target participant in target, and name the case in context. CaseParticipantRole is a Vultron vocabulary type.

    Offer case participant role

    Offers one role on the case to the participant named by the target. The Case Manager records the offer and forwards it to that participant.

    • When… the Case Owner offers a CVD role on the case to another participant
    • Send: Offer(CaseParticipantRole)[context=VulnerabilityCase]
    • Send to: the case manager
    • Sent by: the case owner
    • Reference: Offer case participant role
    Example activity
    {
      "@context": "https://www.w3.org/ns/activitystreams",
      "type": "Offer",
      "id": "urn:uuid:e845328f-90d4-4d9c-ab3f-ea9de2207719",
      "name": "https://vultron.example/organizations/vendorco Offer urn:uuid:a27f0844-948c-43a7-ab8c-8d1bf5447540 to Coordinator LLC",
      "context": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0",
      "content": "We're offering the coordinator role on case VendorCo Case #70930046 to Coordinator LLC.",
      "to": [
        "https://vultron.example/case-actors/vendorco-case-actor"
      ],
      "actor": "https://vultron.example/organizations/vendorco",
      "target": {
        "@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": []
        }
      },
      "object": {
        "@context": "https://certcc.github.io/Vultron/ns/context.jsonld",
        "type": "CaseParticipantRole",
        "id": "urn:uuid:a27f0844-948c-43a7-ab8c-8d1bf5447540",
        "published": "2026-10-23T20:17:08+00:00",
        "updated": "2026-10-23T20:17:08+00:00",
        "role": "coordinator"
      }
    }
    
  3. Wait for the reply. Nothing changes until the target answers.

The Case Manager refuses the offer, records nothing and forwards nothing when any of these is true:

  • you are not the Case Owner;
  • the offer carries no readable role, or offers CASE_OWNER or CASE_MANAGER;
  • the target is not a participant in the case, or already holds the role.

When it admits the offer, the Case Manager records it in the case ledger and then sends the target its own Offer(CaseParticipantRole). The forward names the Case Manager as its actor and you as its attributedTo, and its inReplyTo is the id of the offer the Case Manager recorded. The Case Manager does not give the target the role at this point.

In the reference implementation this is the offer-case-participant-role trigger behavior. The trigger addresses the offer to the case's Case Manager, and fails without sending anything when the case names no Case Manager. The request must name the role to offer: it has no default. No vultron-demo scenario exercises role delegation yet; drive it through the Trigger API endpoints listed under Further reading.

Offer a role, not a case

A role offer and a case ownership transfer are both offers, and they used to serialize identically. Carrying a CaseParticipantRole object is what tells a peer which one you mean (ADR-0039), so do not offer the VulnerabilityCase when you mean to delegate a role. For the transfer, see Ownership Transfer.


Answer a role offer

Act only on a role offer the Case Manager sent you. A role offer from any other actor, the Case Owner included, is refused with no answer (CM-33-004).

Reply to the Case Manager only, never to the Case Owner, with the forwarded Offer as your object.

  • If you are taking the role, send Accept(Offer(CaseParticipantRole)).

    Accept case participant role

    Accepts the offered role. The participant holds the role once the Case Manager records the acceptance.

    • When… a participant accepts a role offered to it on the case
    • Send: Accept(Offer(CaseParticipantRole)[context=VulnerabilityCase])
    • Send to: the case manager
    • Sent by: the addressee of the case participant role offer
    • Reference: Accept case participant role
    Example activity
    {
      "@context": "https://www.w3.org/ns/activitystreams",
      "type": "Accept",
      "id": "urn:uuid:40444c3d-faa6-4307-8ec9-f9913a57605f",
      "name": "https://vultron.example/organizations/coordinator Accept https://vultron.example/case-actors/vendorco-case-actor Offer urn:uuid:265a2426-8202-4824-a381-d39e02af2adf to Coordinator LLC",
      "context": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0",
      "content": "We're accepting the role you offered.",
      "to": [
        "https://vultron.example/case-actors/vendorco-case-actor"
      ],
      "actor": "https://vultron.example/organizations/coordinator",
      "object": {
        "@context": "https://www.w3.org/ns/activitystreams",
        "type": "Offer",
        "id": "urn:uuid:77db7425-4f65-4861-bfee-f3840c33ae1d",
        "name": "https://vultron.example/case-actors/vendorco-case-actor Offer urn:uuid:265a2426-8202-4824-a381-d39e02af2adf to Coordinator LLC",
        "context": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0",
        "inReplyTo": "urn:uuid:16090a03-f4ac-4465-9273-608e8f2e3ad8",
        "published": "2026-10-23T20:17:08+00:00",
        "updated": "2026-10-23T20:17:08+00:00",
        "content": "We're offering the coordinator role on case VendorCo Case #70930046 to Coordinator LLC.",
        "to": [
          "https://vultron.example/organizations/coordinator"
        ],
        "attributedTo": "https://vultron.example/organizations/vendorco",
        "actor": "https://vultron.example/case-actors/vendorco-case-actor",
        "target": {
          "@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": []
          }
        },
        "object": {
          "@context": "https://certcc.github.io/Vultron/ns/context.jsonld",
          "type": "CaseParticipantRole",
          "id": "urn:uuid:265a2426-8202-4824-a381-d39e02af2adf",
          "published": "2026-10-23T20:17:08+00:00",
          "updated": "2026-10-23T20:17:08+00:00",
          "role": "coordinator"
        }
      }
    }
    
  • If you are not, send Reject(Offer(CaseParticipantRole)). The roster is unchanged.

    Reject case participant role

    Declines the offered role. The roster does not change.

    • When… a participant declines a role offered to it on the case
    • Send: Reject(Offer(CaseParticipantRole)[context=VulnerabilityCase])
    • Send to: the case manager
    • Sent by: the addressee of the case participant role offer
    • Reference: Reject case participant role
    Example activity
    {
      "@context": "https://www.w3.org/ns/activitystreams",
      "type": "Reject",
      "id": "urn:uuid:9a141499-be93-4f88-b4f3-dbeed1a7fc11",
      "name": "https://vultron.example/organizations/coordinator Reject https://vultron.example/case-actors/vendorco-case-actor Offer urn:uuid:51621074-e78d-450e-9a34-635107349549 to Coordinator LLC",
      "context": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0",
      "content": "We're declining the role you offered.",
      "to": [
        "https://vultron.example/case-actors/vendorco-case-actor"
      ],
      "actor": "https://vultron.example/organizations/coordinator",
      "object": {
        "@context": "https://www.w3.org/ns/activitystreams",
        "type": "Offer",
        "id": "urn:uuid:47a9308f-17ad-48b2-ae62-44fa83968f23",
        "name": "https://vultron.example/case-actors/vendorco-case-actor Offer urn:uuid:51621074-e78d-450e-9a34-635107349549 to Coordinator LLC",
        "context": "urn:uuid:d483d06d-400e-47fd-9b8a-9a2643e3a4b0",
        "inReplyTo": "urn:uuid:adbfb874-d5d9-4904-a2f3-644696cd7255",
        "published": "2026-10-23T20:17:08+00:00",
        "updated": "2026-10-23T20:17:08+00:00",
        "content": "We're offering the coordinator role on case VendorCo Case #70930046 to Coordinator LLC.",
        "to": [
          "https://vultron.example/organizations/coordinator"
        ],
        "attributedTo": "https://vultron.example/organizations/vendorco",
        "actor": "https://vultron.example/case-actors/vendorco-case-actor",
        "target": {
          "@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": []
          }
        },
        "object": {
          "@context": "https://certcc.github.io/Vultron/ns/context.jsonld",
          "type": "CaseParticipantRole",
          "id": "urn:uuid:51621074-e78d-450e-9a34-635107349549",
          "published": "2026-10-23T20:17:08+00:00",
          "updated": "2026-10-23T20:17:08+00:00",
          "role": "coordinator"
        }
      }
    }
    

The Case Manager acts on your answer only if you are the target of a role offer it recorded, and only while that offer is open (CM-33-005). The offer is open until the Case Manager records an Accept or Reject of it. An answer that names the forward is matched to the recorded offer through the forward's inReplyTo, never through the copy of the offer your answer carries. That copy must still name the offered role and you as the target, because every participant reads the grant from it, so the role granted is always the one the case offered and never one your answer names. The Case Manager refuses an answer from anyone else, an answer to an offer that is no longer open, and an answer whose copy names another role or target, and records nothing for it. If it refuses your own answer because the offer is no longer open or your copy differs, it sends you the refusal notice: a Reject of your answer, with the reason in summary. If your answer is delivered again after the Case Manager recorded it, the Case Manager changes nothing.

When it admits your answer, the Case Manager records it in the case ledger and announces the entry to every participant in the case (CM-33-006). On an Accept, the Case Manager grants the role on its own copy of the case, and every other participant grants it when it replays the entry. You hold the role once the Case Manager records your Accept in the case ledger (CM-02-016). Consider what the role obliges you to do before accepting.


Verify

What you sent What to confirm
Offer(CaseParticipantRole) The Case Manager's ledger holds an offer_case_participant_role entry, and the target holds the Case Manager's forward.
Accept(Offer(CaseParticipantRole)) The Case Manager's ledger holds an accept_case_participant_role entry, and the role appears on your participant record once the Case Manager's entry is replayed on your copy of the case.
Reject(Offer(CaseParticipantRole)) The Case Manager's ledger holds a reject_case_participant_role entry, and your roles are unchanged.

Only the Case Manager writes to the case ledger. Your answer adds no entry to your own copy of the ledger; the Case Manager records it and announces the entry to every participant. The role appears only once the Case Manager's entry is replayed, never from your Accept itself.


Further reading