Composing SPP bodies
In SPP, a stage's decision-makers, its bodies, are other contracts: existing governance plugins, a Safe, even an EOA. This page is the integration contract: how a body plugs in, and how to make your own plugin usable as one.
Acting in a body decides the parent, not the actions. When someone votes or approves inside a body, they're voting on whether to advance or veto the SPP proposal at that stage, not on the proposal's underlying actions directly. The body is a gate; SPP owns the actions and the final execution.
Automatic vs. manual bodies
Every body is one of two kinds (configured per stage). The split comes down to how SPP learns the body's verdict, which depends on whether the body speaks OSx's proposal interface:
- Automatic (
isManual = false): the body is an OSx-native plugin that reports success throughIProposal. So SPP can create a sub-proposal on it when the stage begins (its single action is a callback toreportProposalResultpre-filled with the body's verdict), and SPP can poll the body'shasSucceededdirectly, no custom wiring either way. Requires ERC-165IProposalsupport and that SPP holdsCREATE_PROPOSAL_PERMISSIONon the body. - Manual (
isManual = true): the body is something that doesn't implement OSx's proposal interface, a Safe, a vault, an external Governor, an EOA, so there's nohasSucceededfor SPP to poll and no sub-proposal to create. Instead the external system pushes its verdict in: it callsreportProposalResultto notify SPP that it has approved or vetoed. Manual mode is the interoperability escape hatch that lets any external address take part in a stage without adopting a single Aragon interface. The trade-off is that someone has to drive that reporting; it isn't automated for you.
Making your plugin an automatic body
Inherit the shared Proposal/ProposalUpgradeable base (it gives you IProposal + ERC-165), then honor three rules:
createProposalmust accept SPP's call and add no extra gatekeeping. SPP passes standard proposal params (metadata, actions, start/end dates, and adatablob for anything plugin-specific, declare its shape withcustomProposalParamsABIso SPP and UIs can encode it). Critically, do not add bespoke eligibility checks insidecreateProposal(token-balance requirements, allowlists, etc.). SPP relies purely on the OSx permission system to decide whether it may create the sub-proposal; any extra check can make SPP's call fail, and the body then silently drops out of the stage (aSubProposalNotCreatedevent, no revert).hasSucceededmust be monotonic, once it returnstrue, it must staytrueforever. SPP polls it (the pull tally) possibly long after your body's own voting window closed, while other bodies in the stage are still deciding. AhasSucceededthat reverts tofalseafter the window strands the SPP proposal.canExecuteis yours to define. SPP never calls it, so it's free to enforce time windows or other conditions for the body's own execution; that doesn't affect SPP.
Using a manual body (a Safe)
For a body that can't be automatic, register its address with isManual = true and report results yourself. The canonical example is a Safe as a veto body:
// After the SPP proposal exists, submit this as a Safe transaction:
// to: <SPP address>
// data:
abi.encodeCall(
StagedProposalProcessor.reportProposalResult,
(proposalId, stageId, ResultType.Veto, /* tryAdvance */ false)
);stageIdmust be the stage this body sits in. Reporting for a stage the proposal hasn't reached yet reverts (StageIdInvalid); a report for the current or an earlier stage from an address that isn't a registered body of it is silently ignored (a wasted-gas no-op). So target the exact stage, and only where you're actually a body.- Do it before the stage's
maxAdvance, or the proposal expires regardless. tryAdvance: truealso advances the proposal in the same transaction if the reported result is enough and the caller holdsADVANCE_PERMISSION_ID.
Which plugins can be bodies
Any address can be a manual body. For automatic bodies, the standard OSx governance plugins already qualify (they inherit the proposal base): Multisig, Token Voting, and Admin. A Safe or an external Governor is the typical manual body. This is what makes the canonical pitch real: multisig approves → token holders vote → admin executes, each an existing plugin, stitched into a pipeline by SPP.
SPP inside SPP
Because SPP implements IProposal, an SPP instance can be a body of another SPP, nesting pipelines (a whole sub-pipeline standing in as one stage's decision). It plugs in like any automatic body, with two things to remember: the inner SPP needs its own stages, bodies, and permissions configured, and its verdict reaches the outer SPP only when the inner pipeline is actually executed (execution fires the report callback, and since executing is permissionless anyone can trigger it once it's ready). Note the inner SPP's own hasSucceeded is strictly monotonic only after it executes, a last-stage-advanceable inner proposal that hits maxAdvance expires back to un-succeeded, so nesting leans on the inner pipeline being executed, not merely polled.
Keep in mind
- Never add custom access control to a body's
createProposalbeyond OSx permissions, it breaks SPP's automatic sub-proposal creation, and the body silently stops counting. - A body's
hasSucceededmust never go back to false, or it can permanently stall the SPP proposal. - SPP needs
CREATE_PROPOSAL_PERMISSIONon each automatic body; without it, that body degrades toSubProposalNotCreatedand contributes nothing.
See also
- Lifecycle & state machine — the push/pull tally that consumes
hasSucceeded. - Stages & bodies — where a body's kind and result type are set.
- Multisig, Token Voting, Admin — the composable bodies.