Core Resources
View as Markdown

Sign-off

Where a decision stands and what you can do about it: who needs to weigh in, how each person has responded, the recorded call — and, scoped to you, the actions you can take right now.

A decision is a Droplet wearing the decision Type (see the Decisions concept). Its sign-off is the live state of that decision: the RAPID roster, each approver's response, and the decider's recorded call. There is no separate "decision" object — the Droplet holds the content, the Type makes it a decision, and this resource is how you read and drive its sign-off.

The read (get) is affordance-bearing. It returns the current state and the actions available to you, each with a reason when it isn't (e.g. "only the decider can record the call"). A person — or an agent — can act on what's available without having to know the rules.

What you can do:

  • cast — record a response as an approver: agree, agree with a condition, or disagree (a comment is required for the latter two). Pass onBehalfOf to record someone else's position — see Recording for someone else below.
  • clear — remove a recorded response while sign-off is still open. Yours, or (as a facilitator) one you recorded for someone.
  • finalize — the decider records the call: approve → decided, block → blocked. (In practice the decider's own cast records it.)
  • reopen — clear a recorded call and return the decision to open.
  • open / close — an editor or facilitator turns sign-off on (draft → in flight) or back off (→ draft); responses are retained either way. To take the decision off the Droplet altogether, clear its Type (droplet.clearType) — that discards the roster, the responses and the call, and reports what it took.
  • set-proposition — an editor or facilitator sets "what you're signing off on", the statement shown above the roster.
  • mine — across every decision you can reach, the ones waiting on you.
  • list — every decision you can see, each with its sign-off state; filter by phase, your role, or tag. (The query behind the Decisions dashboard.)

Recording for someone else. Decisions get made in rooms, and the person running the room is often not the person holding the role. Members of the @drop-facilitators group (and editors on the Droplet) can pass onBehalfOf to cast and clear, and can drive the lifecycle — open, close, set-proposition, and the roster — on any decision they can already read. Group membership grants no access of its own, and never unlocks editing the Droplet itself.

A proxied response carries castBy on the roster row, and the event log records both who acted and who it was for. The subject overwrites it by casting themselves, which clears castBy and makes the row self-affirmed again. Recording the decider's response records the call, exactly as their own cast would — attributed to them, and reopenable by them.

Everything else is scoped to the calling identity: you only see and act on decisions you can access, in the roles you actually hold. The behavior is generic — a Type declares which of its user-list fields carry the approver and decider roles and which status values gate the flow — so the same surface works for any sign-off-shaped Type. A RAPID decision is the first one.

The Sign-off object

Every Sign-off carries these attributes.

Attributes

configured
bool
Whether the Droplet's Type is sign-off-shaped. When false, there is no sign-off — the rest of the object is empty.
phase
string
Lifecycle phase: draft (not yet open), open (collecting sign-offs), decided, or blocked.
status
string· nullable
The raw value of the Type's status field that the phase is derived from.
proposition
string· nullable
The editor-authored statement of what people are signing off on, shown above the roster.
roster
json
Every RAPID role on the Type (Recommend / Agree / Perform / Input / Decide) with its members. isApprover marks the roles that cast sign-offs. Members are people or teams; a team in an approver role carries a tally of how its members voted.
approvers
json
The flat approver list with each response: { email, name, vote (agree|condition|disagree|null), comment, castBy }. vote: null = hasn't weighed in. castBy names the facilitator who recorded it for them, and is null when they recorded it themselves.
signedCount
int
Approvers who have responded (agree or condition).
totalApprovers
int
Total approvers across the approver roles (teams expanded to members).
verdict
json· nullable
The recorded call once decided: { outcome (approved|blocked), summary, decidedBy, decidedAt }. Null while open.
openedAt
string· nullable
When sign-off opened (ISO). Powers the "in flight · 3d" age. Null while draft.
viewer
json
The caller's standing: { email, isApprover, isDecider, vote, comment, canManage, canFacilitate, castBy }. canManage = editor on the Droplet. canFacilitate = may drive this decision (record for others, open/close, proposition, roster) — true for editors and for @drop-facilitators members who can read it. castBy is set when your own response was recorded by someone else.
canClose
bool
Whether the caller can un-enable (return to draft) right now — editor or facilitator, open phase, Type has a draft status.
actions
json
Affordances for the caller: { name (cast|clear|finalize|reopen|cast-for-others), available, reason? }. Act on available; surface reason when blocked. cast-for-others answers "may I record someone else's position", which cast (your own) does not.
THE SIGN-OFF OBJECT
{
  "configured": true,
  "phase": "open",
  "status": "in-flight",
  "proposition": "Adopt RDS Proxy in front of the SSR fleet to end burst-time connection storms.",
  "roster": [
    {
      "key": "recommend",
      "label": "Recommend",
      "isApprover": false,
      "members": [
        {
          "kind": "user",
          "email": "kate@himarley.com",
          "name": "Kate Horning"
        }
      ]
    },
    {
      "key": "agree",
      "label": "Agree",
      "isApprover": true,
      "members": [
        {
          "kind": "group",
          "slug": "platform-team",
          "name": "Platform Team",
          "memberCount": 3,
          "hasViewer": true,
          "tally": {
            "agree": 1,
            "condition": 1,
            "disagree": 0,
            "pending": 1
          }
        }
      ]
    },
    {
      "key": "decide",
      "label": "Decide",
      "isApprover": false,
      "members": [
        {
          "kind": "user",
          "email": "will@himarley.com",
          "name": "Will Sherlin"
        }
      ]
    }
  ],
  "approvers": [
    {
      "email": "you@himarley.com",
      "name": "You",
      "vote": null,
      "comment": ""
    },
    {
      "email": "sarah@himarley.com",
      "name": "Sarah Lee",
      "vote": "condition",
      "comment": "Confirm the rollback plan first",
      "castBy": "kate@himarley.com"
    }
  ],
  "signedCount": 1,
  "totalApprovers": 3,
  "verdict": null,
  "openedAt": "2026-08-05T14:00:00.000Z",
  "viewer": {
    "email": "you@himarley.com",
    "isApprover": true,
    "isDecider": false,
    "vote": null,
    "canManage": false,
    "canFacilitate": false,
    "castBy": null
  },
  "canClose": false,
  "actions": [
    {
      "name": "cast",
      "available": true
    },
    {
      "name": "clear",
      "available": false,
      "reason": "you have not signed off yet"
    },
    {
      "name": "finalize",
      "available": false,
      "reason": "only the decider can record the call"
    },
    {
      "name": "reopen",
      "available": false,
      "reason": "nothing to reopen — no verdict recorded"
    },
    {
      "name": "cast-for-others",
      "available": false,
      "reason": "you are not a facilitator or editor on this decision"
    }
  ]
}

Methods

Endpoints to operate on a Sign-off. The example switches between curl, drop CLI, and MCP via the dropdown — your choice persists across the page.

GET

Get sign-off

/api/apps/{slug}/signoff

Read a Droplet's sign-off state — roles, responses, verdict, status — plus the actions available to you (an affordance-bearing read). Blocked actions carry a reason. Everything is scoped to the calling identity: you only see what you can act on.

GET/api/apps/{slug}/signoff
drop signoff get migrate-drop-to-ai
Response200
{
  "configured": false,
  "status": "example",
  "phase": "draft",
  "proposition": "example",
  "roster": [],
  "approvers": [],
  "signedCount": 0,
  "totalApprovers": 0,
  "responseCount": 0,
  "verdict": {
    "outcome": "approved",
    "summary": "example",
    "decidedBy": "example",
    "decidedAt": "example"
  },
  "openedAt": "example",
  "viewer": {
    "email": "example",
    "isApprover": false,
    "isDecider": false,
    "vote": "agree",
    "comment": "example",
    "canManage": false,
    "canFacilitate": false,
    "castBy": "example",
    "castByName": "example"
  },
  "canClose": false,
  "blockedApprovers": [],
  "actions": []
}
POST

Cast sign-off

/api/apps/{slug}/signoff

Record a response as an approver: agree, condition (agree with a condition), or disagree. A comment is required for condition and disagree. Approver-gated — the subject must hold an approver or decider role on the Droplet's Type. Casting again updates the response; use clear to remove it.

Pass onBehalfOf to record someone else's position, which is what a facilitator does live in a meeting. The response is attributed (castBy), the event log records both who acted and who it was for, and the subject can overwrite it themselves at any time — which clears the attribution.

POST/api/apps/{slug}/signoff
drop signoff cast migrate-drop-to-ai --vote agree
Response200
{
  "configured": false,
  "status": "example",
  "phase": "draft",
  "proposition": "example",
  "roster": [],
  "approvers": [],
  "signedCount": 0,
  "totalApprovers": 0,
  "responseCount": 0,
  "verdict": {
    "outcome": "approved",
    "summary": "example",
    "decidedBy": "example",
    "decidedAt": "example"
  },
  "openedAt": "example",
  "viewer": {
    "email": "example",
    "isApprover": false,
    "isDecider": false,
    "vote": "agree",
    "comment": "example",
    "canManage": false,
    "canFacilitate": false,
    "castBy": "example",
    "castByName": "example"
  },
  "canClose": false,
  "blockedApprovers": [],
  "actions": []
}
POST

Clear sign-off

/api/apps/{slug}/signoff/clear

Remove a recorded sign-off response while the decision is still open. Clears your own by default; a facilitator can pass onBehalfOf to remove a response they recorded for someone. Finalized decisions must be reopened before responses can change.

POST/api/apps/{slug}/signoff/clear
drop signoff clear migrate-drop-to-ai
Response200
{
  "configured": false,
  "status": "example",
  "phase": "draft",
  "proposition": "example",
  "roster": [],
  "approvers": [],
  "signedCount": 0,
  "totalApprovers": 0,
  "responseCount": 0,
  "verdict": {
    "outcome": "approved",
    "summary": "example",
    "decidedBy": "example",
    "decidedAt": "example"
  },
  "openedAt": "example",
  "viewer": {
    "email": "example",
    "isApprover": false,
    "isDecider": false,
    "vote": "agree",
    "comment": "example",
    "canManage": false,
    "canFacilitate": false,
    "castBy": "example",
    "castByName": "example"
  },
  "canClose": false,
  "blockedApprovers": [],
  "actions": []
}
POST

Finalize sign-off

/api/apps/{slug}/signoff/finalize

Record the decider's call and advance the Droplet's status (approve → decided, block → blocked). Decider-gated — the caller must hold the decider role. A summary is required.

POST/api/apps/{slug}/signoff/finalize
drop signoff finalize migrate-drop-to-ai --outcome approved --summary "Approved — migrate to himarley.ai over two weeks."
Response200
{
  "configured": false,
  "status": "example",
  "phase": "draft",
  "proposition": "example",
  "roster": [],
  "approvers": [],
  "signedCount": 0,
  "totalApprovers": 0,
  "responseCount": 0,
  "verdict": {
    "outcome": "approved",
    "summary": "example",
    "decidedBy": "example",
    "decidedAt": "example"
  },
  "openedAt": "example",
  "viewer": {
    "email": "example",
    "isApprover": false,
    "isDecider": false,
    "vote": "agree",
    "comment": "example",
    "canManage": false,
    "canFacilitate": false,
    "castBy": "example",
    "castByName": "example"
  },
  "canClose": false,
  "blockedApprovers": [],
  "actions": []
}
POST

Reopen sign-off

/api/apps/{slug}/signoff/reopen

Clear a recorded verdict and return the Droplet to the open status so approvers can sign off again. Open to the decider AND to facilitators — a facilitator can record the decider's call, so they have to be able to unmake it, or proxying a verdict would be a one-way door.

POST/api/apps/{slug}/signoff/reopen
drop signoff reopen migrate-drop-to-ai
Response200
{
  "configured": false,
  "status": "example",
  "phase": "draft",
  "proposition": "example",
  "roster": [],
  "approvers": [],
  "signedCount": 0,
  "totalApprovers": 0,
  "responseCount": 0,
  "verdict": {
    "outcome": "approved",
    "summary": "example",
    "decidedBy": "example",
    "decidedAt": "example"
  },
  "openedAt": "example",
  "viewer": {
    "email": "example",
    "isApprover": false,
    "isDecider": false,
    "vote": "agree",
    "comment": "example",
    "canManage": false,
    "canFacilitate": false,
    "castBy": "example",
    "castByName": "example"
  },
  "canClose": false,
  "blockedApprovers": [],
  "actions": []
}
POST

Open for sign-off

/api/apps/{slug}/signoff/open

Open a draft decision for sign-off — flips its status to the open value so approvers can respond. Facilitator-gated: editors on the Droplet, plus @drop-facilitators members who can read it. Backs the "Enable sign-off" author action and the rapid agent opening the room after staffing.

POST/api/apps/{slug}/signoff/open
drop signoff open migrate-drop-to-ai
Response200
{
  "configured": false,
  "status": "example",
  "phase": "draft",
  "proposition": "example",
  "roster": [],
  "approvers": [],
  "signedCount": 0,
  "totalApprovers": 0,
  "responseCount": 0,
  "verdict": {
    "outcome": "approved",
    "summary": "example",
    "decidedBy": "example",
    "decidedAt": "example"
  },
  "openedAt": "example",
  "viewer": {
    "email": "example",
    "isApprover": false,
    "isDecider": false,
    "vote": "agree",
    "comment": "example",
    "canManage": false,
    "canFacilitate": false,
    "castBy": "example",
    "castByName": "example"
  },
  "canClose": false,
  "blockedApprovers": [],
  "actions": []
}
POST

Close (un-enable) sign-off

/api/apps/{slug}/signoff/close

Un-enable sign-off — return an open decision to draft. Facilitator-gated: editors on the Droplet, plus @drop-facilitators members who can read it. The inverse of signoff.open; retains responses so re-opening keeps prior sign-offs.

POST/api/apps/{slug}/signoff/close
drop signoff close migrate-drop-to-ai
Response200
{
  "configured": false,
  "status": "example",
  "phase": "draft",
  "proposition": "example",
  "roster": [],
  "approvers": [],
  "signedCount": 0,
  "totalApprovers": 0,
  "responseCount": 0,
  "verdict": {
    "outcome": "approved",
    "summary": "example",
    "decidedBy": "example",
    "decidedAt": "example"
  },
  "openedAt": "example",
  "viewer": {
    "email": "example",
    "isApprover": false,
    "isDecider": false,
    "vote": "agree",
    "comment": "example",
    "canManage": false,
    "canFacilitate": false,
    "castBy": "example",
    "castByName": "example"
  },
  "canClose": false,
  "blockedApprovers": [],
  "actions": []
}
POST

Set the proposition

/api/apps/{slug}/signoff/proposition

Set "what you're signing off on" — the succinct statement shown above the sign-off. Facilitator-gated (canFacilitateSignoff): editors on the Droplet, plus @drop-facilitators members who can read it. The tray's inline editor and the rapid agent both drive this; pass an empty string to clear it.

POST/api/apps/{slug}/signoff/proposition
drop signoff set-proposition migrate-drop-to-ai --proposition "Adopt RDS Proxy in front of the SSR fleet to end burst-time connection storms."
Response200
{
  "configured": false,
  "status": "example",
  "phase": "draft",
  "proposition": "example",
  "roster": [],
  "approvers": [],
  "signedCount": 0,
  "totalApprovers": 0,
  "responseCount": 0,
  "verdict": {
    "outcome": "approved",
    "summary": "example",
    "decidedBy": "example",
    "decidedAt": "example"
  },
  "openedAt": "example",
  "viewer": {
    "email": "example",
    "isApprover": false,
    "isDecider": false,
    "vote": "agree",
    "comment": "example",
    "canManage": false,
    "canFacilitate": false,
    "castBy": "example",
    "castByName": "example"
  },
  "canClose": false,
  "blockedApprovers": [],
  "actions": []
}
GET

Sign-offs awaiting me

/api/signoff/mine

List the sign-offs across all Droplets you can reach where you hold a role — filtered to those awaiting your action. Powers a personal "decisions awaiting your sign-off" inbox and lets an agent ask "what's blocked on me?"

GET/api/signoff/mine
drop signoff mine
Response200
{
  "items": []
}
GET

List decisions

/api/signoff

List every decision you can see, each with its sign-off state (phase, who decides, how far along). Optionally filter by phase, your role (decider|approver), or tag. This is the query behind the /decisions dashboard — the answer to "list all in-flight decisions and where each stands" or "decisions where I'm the decider".

GET/api/signoff
drop signoff list
Response200
{
  "items": []
}