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 portal/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 portal/src/retainer_walk.rs:
// Idempotency: this notation already has an envelope out. Reuse the
// recorded id, fire nothing, send nothing — the post-state is
// whatever the notation already records.
if let Some(existing) =
store::signatures::request_id_for_notation(deps.surreal, notation_id).await?
{
return Ok((
StateName::from(notation_row.state.as_str()),
crate::signature::SignatureRequestId(existing),
));
}