How to Run a Statement of Work Through a Multi-Slot Approval Before Signature

When a client emails a Statement of Work PDF to a Spojit mailhook address, this workflow pulls the file, summarizes its terms against your standard-terms collection, gates the run behind separate legal and finance approval slots, and only then emails a countersigned acknowledgement back to the client.

What This Integration Does

Statements of Work arrive as PDF attachments and need two different sets of eyes before anyone signs: legal checks the liability, indemnity, and IP clauses, while finance checks the payment terms, caps, and milestones. Doing this by forwarding emails around is slow and easy to lose track of. This Spojit workflow turns the inbound SOW into a single auditable run: the PDF is captured automatically, the intelligent layer across your automation (Miraxa) reads it and flags how each term compares to your firm's standard position, and a single Human node holds the run until both legal and finance have signed off. Only when every approval slot is satisfied does the client receive an acknowledgement that you have accepted the SOW.

The run is triggered by mail landing on a Mailhook trigger, so it starts within seconds and needs no mailbox or OAuth. The SOW bytes flow from an Attachment node into a transient Knowledge query that compares the document to your persistent standard-terms collection. That summary is shown to approvers in the Human node. Approval uses AND semantics across two slots, so the run continues only after both teams approve; a rejection or timeout halts the run and no acknowledgement is sent. Each inbound message is deduplicated, so a client resending the same email will not double-process. Re-running is safe: nothing is written to an external system until the final Resend send, which fires once per approved run.

Prerequisites

  • A workspace where you can create workflows and manage the Knowledge section.
  • A persistent Knowledge collection holding your standard SOW terms (your master services agreement, rate card, liability caps, and accepted clause language) uploaded as documents and embedded.
  • A Resend connection (API key) with a verified sending domain, so the acknowledgement comes from your own address. See the Resend connector reference below.
  • A Slack connection if you want an internal heads-up posted when an SOW arrives (optional).
  • Users, roles, or teams in your workspace that represent your legal reviewers and your finance reviewers, so you can target each approval slot.

Step 1: Capture the SOW with a Mailhook trigger

Open a new workflow in the Spojit designer and set the Trigger node type to Mailhook. Set an Address prefix such as sow (1 to 24 characters), then choose Generate email address to mint a unique address like sow-1a2b3c4d5e6f7g8h@mailhook.spojit.com. Copy that address and give it to clients, or point a forwarding rule at it from your contracts inbox.

Add a From allowlist of your known client domains and a Subject regex such as (?i)\bSOW|statement of work\b so only genuine SOW emails start a run. The trigger output is available as {{ input }} and includes {{ input.from }}, {{ input.subject }}, {{ input.replyTo }}, and {{ input.attachments }} (each a reference of { id, filename, contentType }). Mailhook runs are always asynchronous, so the client gets no automatic reply; you will send one yourself in the final step.

Step 2: Fetch the SOW PDF with an Attachment node

Add an Attachment node. It only saves on a Mailhook workflow, which is exactly what you have. Set Mode to Single so you get the first matching file as one object. Set the Content type filter to application/pdf and a Filename pattern of *.pdf to ignore signature images or logos in the email. Turn on Fail if no attachment matches so an SOW email with no PDF stops cleanly rather than continuing with empty data.

Name the output variable sow. In Single mode the node returns:

{
  "filename": "client-sow-2026.pdf",
  "contentType": "application/pdf",
  "size": 184320,
  "content": "<base64 bytes of the PDF>"
}

The content field is base64 and feeds straight into the next step. Keep individual attachments under the 10 MB limit (25 MB per run).

Step 3: Compare the SOW to your standard terms with a Knowledge node

You need the SOW text available to query, and you want to compare it against your firm's standard position. Add a Knowledge node in Embed mode first: set Collection to Transient (auto-created for this run, cleaned up at the end), Document Type to PDF, and Document Input to {{ sow.content }}. Name the output variable embedResult. Because the collection is transient, no file name is required.

Add a second Knowledge node in Query mode pointed at your persistent standard-terms Collection (the one you prepared in Prerequisites), and set the Result Count to 5. Write a Prompt that asks the intelligent layer to read the inbound SOW and contrast it with your standard terms, for example:

Summarize this incoming Statement of Work and compare each
key term (payment schedule, liability cap, indemnity, IP
ownership, termination) against our standard terms. For each
term, state whether it MATCHES, is MORE FAVORABLE to the
client, or is a DEVIATION that needs sign-off. The SOW text
follows: {{ sow.content }}

Add a Response Schema so the result is structured rather than free prose, which keeps the approval message tidy:

{
  "type": "object",
  "properties": {
    "summary": { "type": "string" },
    "paymentTerms": { "type": "string" },
    "liabilityCap": { "type": "string" },
    "deviations": { "type": "array", "items": { "type": "string" } },
    "recommendation": { "type": "string" }
  },
  "required": ["summary", "deviations", "recommendation"]
}

Name the output variable review. Always use the same embedding model your standard-terms collection was created with so the query matches correctly.

Step 4: Notify the internal team (optional) with a Slack Connector node

If you want reviewers nudged the moment an SOW lands, add a Connector node on the Slack connector in Direct mode and pick the send-message tool. Map channel to your contracts channel (for example #contracts-review) and set the message text using the structured review:

New SOW from {{ input.from }} - {{ input.subject }}

Summary: {{ review.summary }}
Deviations: {{ review.deviations }}
Recommendation: {{ review.recommendation }}

Approve in Spojit: legal + finance sign-off required.

Direct mode is deterministic and costs no AI credits, which is right for a single predictable post. This step is informational only; the gate is the next node.

Step 5: Gate the run with a multi-slot Human approval node

Add a Human node. This is where the two-team sign-off happens. Set a Label like SOW sign-off and a Message that shows approvers exactly what they are deciding on, drawing from the review variable:

SOW from {{ input.from }}: {{ input.subject }}

{{ review.summary }}

Deviations from standard terms:
{{ review.deviations }}

Recommendation: {{ review.recommendation }}

The only required field is Approval slots. Create two slots: a Legal slot and a Finance slot. In each slot add the atoms (a specific User, a Role, or a Team) who can satisfy it. Any one atom satisfies its own slot, but the run continues only when EVERY slot is satisfied, so legal and finance must both approve. Set a Timeout (minutes) if SOWs must be actioned within, say, two business days; leave it blank for no deadline. Turn on Email approvers and set an Urgency of High for time-sensitive deals.

Approvers act in the Approvals inbox at /approvals, on the dashboard widget, or via the email link. On approval the node outputs { approved: true, approvalId, outcome: "APPROVED" } and the run continues. A rejection or a timeout halts the workflow, so nothing downstream runs and no acknowledgement is sent. There is no "on reject do X" branch: a reject ends the run, which is the safe default for a pre-signature gate.

Step 6: Send the countersigned acknowledgement with a Resend Connector node

This step is only reached after both slots approve. Add a Connector node on the Resend connector in Direct mode and pick the send-email tool so the message comes from your own verified domain. Reply straight to the sender using the mailhook reply address. Map the fields:

  • from: your contracts address on a verified domain, for example contracts@yourfirm.com
  • to: {{ input.replyTo }} (falls back to the original sender)
  • subject: Re: {{ input.subject }} - accepted and countersigned
  • text: a confirmation that legal and finance have approved and you are proceeding
  • attachments: attach the same SOW back as a record, with filename set to {{ sow.filename }} and the base64 content set to {{ sow.content }}

A sample body:

Hi,

Thank you for sending through the Statement of Work
"{{ input.subject }}". Our legal and finance teams have
reviewed and approved it, and we are pleased to confirm our
acceptance. A copy is attached for your records, and we will
follow up shortly to arrange signature.

Kind regards,
Contracts Team

If you would rather use Spojit's built-in mail service and do not need to send from your own domain, you could swap this for a Send Email node instead; note that external recipients must be on the org allowlist under Settings → General → Email recipients. Using the Resend connector here keeps the acknowledgement firmly on your brand.

Tips

  • Keep the standard-terms collection persistent and up to date: when your master services agreement changes, re-upload and re-embed so every future SOW is judged against current language.
  • Use the structured Response Schema from Step 3 in both the Slack message and the Human node message, so legal and finance read the same concise deviation list rather than a wall of PDF text.
  • Set the Human node Urgency and a sensible Timeout (minutes) for high-value or time-boxed deals so stale approvals do not sit open indefinitely.
  • Ask Miraxa to scaffold the skeleton for you with a prompt like "Build a workflow that watches a mailhook, fetches the PDF attachment, queries my standard-terms collection, then gates on a Human approval node with two slots." Then fine-tune each node in the properties panel.

Common Pitfalls

  • Saving an Attachment node on a non-mailhook workflow fails: the designer requires a Mailhook trigger. Build the trigger first.
  • Querying a collection that was embedded with a different embedding model returns poor matches. Use the same embedding model for the standard-terms collection and any embed step that feeds the same query.
  • Forgetting that a single approval slot with both legal and finance users in it would let EITHER person approve alone. To require both teams, you must use TWO separate slots so AND semantics apply across them.
  • Expecting a "rejected" branch to run cleanup or a decline email: rejections and timeouts halt the run. If you need to email the client on decline, handle that as a separate manual step, not a downstream node.
  • Sending from an unverified Resend domain will be refused. Verify your domain (see verify-domain in the connector) before going live.

Testing

Before sharing the mailhook address with clients, email a sample SOW PDF from an address on your From allowlist to the generated mailhook address. Confirm the Attachment node returns a non-empty sow.content, the Knowledge query produces a sensible review.summary and review.deviations, and the run pauses at the Human node. Approve with only the legal slot first and verify the run stays paused (proving AND semantics), then approve the finance slot and confirm the acknowledgement email arrives with the SOW attached. Once the dry run is clean, roll the address out to clients.

Learn More

Did this answer your question? Thanks for the feedback There was a problem submitting your feedback. Please try again later.