Step 5 — signature is a modular step
sent_for_signature__pending is one state in the graph, and the thing that fires it is a small trait — not a vendor.
DocuSign is the shipped implementation; dev and tests run a recording stub, so the step stays testable without an
account.
From web/src/signature.rs:
pub trait SignatureProvider: Send + Sync {
/// Submit the rendered retainer PDF for the given notation, placing
/// the fields described by `manifest`. Returns a provider-issued id
/// correlating future events.
async fn send_for_signature(
&self,
notation_id: Uuid,
pdf: &[u8],
manifest: &SignatureManifest,
) -> Result<SignatureRequestId, SignatureError>;
Because the step is modular it can also be careful. Dispatch is idempotent — a notation that already has an envelope out reuses it, fires nothing, and sends nothing — so a retry can never double-send a client's contract.
From web/src/retainer_walk.rs:
// Idempotency: this notation already has an envelope out. Reuse the
// persisted id, fire nothing, send nothing — the post-state is
// whatever the notation already records.
if let Some(existing) = notation_row.signature_request_id.clone() {
return Ok((
StateName::from(notation_row.state.as_str()),
crate::signature::SignatureRequestId(existing),
));
}