Case Records and Notes Are Attached by One Message, Never Created First
Context and Problem Statement
ActivityStreams lets a sender attach a new object in two ways.
The sender can send one Add that carries the object.
As an alternative, the sender can send a Create that mints the object, and then an Add that attaches it.
The reference implementation attaches a case status and a participant status with one Add, and it carries a proposed embargo event on the Invite that proposes it.
A participant record comes into existence only as a consequence of an act the CASE_MANAGER records: the creation of the case, or the Invite that brings an invitee into the case (ADR-0114).
Its inbox refuses a received Create of any of these objects as an unrecognized activity (#4598).
That was recorded as a choice of the prototype, and the question of what the protocol allows stayed open (#4635, #4676).
A note was the exception.
A note is content that can stand on its own, so Create(Note) minted it.
When the note's context named the case, the CASE_MANAGER attached it to the case on that Create, and a separate Add(Note) attached it as well.
The participant add-note trigger sent the Create, and the CASE_MANAGER sent its own notes (the RM anomaly clarification, the CS invariant diagnostic, the reinforcing-suggestion notice) as Add(Note).
A receiver had to accept both shapes for one act, and nothing said which one the protocol meant (#4550).
This ADR records the maintainer's decisions from live interviews on 2026-10-15.
Decision Drivers
- A case status, a participant status, an embargo proposal and a participant record have no meaning apart from the case they belong to.
- Every note in Vultron is about a case; no protocol act sends a note outside one.
- When ActivityStreams allows several forms for one act, a receiver that must accept all of them carries the cost of every form any sender picks.
- Every change to the case file is its own committed ledger entry, and every committed entry needs a replica apply step or a declared no-replica-effect row (CLP-07-013, RSH-08-004).
- The message vocabulary is being made stable, so a form the protocol allows binds every future implementation.
Considered Options
- One message attaches the record
CreatethenAddCreatewith a casecontextattaches the record
Decision Outcome
Chosen option: "One message attaches the record", because the records have no meaning apart from their attachment, Create then Add adds a "created but not attached" state that nothing needs, and Create with a case context adds a second message for an act that already has one.
The protocol brings each record into the case with exactly one message.
A case status is attached by Add(CaseStatus) with the case as target.
A participant status is attached by Add(ParticipantStatus) with the participant as target.
A proposed embargo event travels on the Invite that proposes it.
A participant record is made by the CASE_MANAGER as a consequence of an act it records, such as the Invite that brings an invitee into the case.
A note is attached by Add(Note) with the case as target, from a participant and from the CASE_MANAGER alike.
A Create of any of these records or of a note, sent to an inbox, is not a protocol message, and a receiver MUST NOT change the case file because of one.
The forms not chosen — Create then Add, a Create whose case context attaches the object, and a note outside any case — are deferred, not rejected: whether the protocol adds any of them is an open question for later protocol design.
This does not cover the Create that a CASE_MANAGER-authored ledger entry carries as its payload snapshot (CLP-07-013): that Create records a change the CASE_MANAGER made, and it is not a message a participant sends.
The protocol does not say how a receiver handles such a Create.
A peer may refuse it, log it or ignore it.
This codebase refuses it as an unrecognized activity, and it carries each record inline on its message; both are rules of this codebase only.
Consequences
- Good, because a record exists in the case only once it is attached, so no "created but not attached" state needs a definition, a ledger entry or a replica step.
- Good, because a status change stays one message and one ledger entry.
- Neutral, because a general ActivityStreams client that mints objects with
Createfirst cannot attach these records that way. - Good, because a note now attaches the way every other case record does, so the vocabulary has one attachment shape and a receiver handles one message per act.
- Bad, because the ActivityPub convention of posting content with
Create(Note)does not attach a note to a case, so a general ActivityPub client must send theAdd.
Validation
The test test_received_create_of_a_case_record_is_refused_case_unchanged in test/adapters/driving/fastapi/test_inbox_outcome_chain.py sends each of the four Create forms to an inbox.
It confirms that the case, the ledger and the outbox stay unchanged.
Pros and Cons of the Options
One message attaches the record
- Good, because it matches what the records mean: each one declares something about the case and nothing apart from it.
- Good, because the reference implementation already works this way and needs no new ledger entries or replica steps.
- Bad, because it adds a protocol restriction that ActivityStreams itself does not make.
Create then Add
- Good, because it is the general ActivityStreams pattern.
- Bad, because each
Createchanges the case file, so each one needs its own committed ledger entry and replica apply step. - Bad, because the protocol must define a record that exists but is not attached.
Create with a case context attaches the record
- Good, because it is one message, like the chosen option.
- Good, because it is how ActivityPub servers post content, and
Create(Note)with a casecontextattached a note this way before this decision. - Bad, because each record would have two attaching messages, the
Createand itsAddorInvite, so a receiver must handle both for one act. - Bad, because each
Createform is a new committed event type, with its own ledger entry and replica apply step, for an act theAddorInvitealready expresses.
More Information
Generated spec requirements: vultron-as2-mapping.yaml VAM-01-011.
VAM-01-010 holds this codebase's inline and refusal rules.
The rule that one protocol act names one wire form, and that the forms not chosen are recorded as an open question, is MS-18-001.