How to Build a Sales Knowledge Base and Answer RFP Questions with Review
Receive an emailed RFP at a Mailhook address, extract the PDF, answer every question against a persistent collection of your past proposals and product docs, route the draft to a sales lead for approval, and only then email the structured answers back to the requester.
What This Integration Does
Sales teams lose hours answering the same RFP and security-questionnaire questions over and over, copying language out of old proposals and product sheets. This Spojit workflow turns that into a single pipeline: a prospect (or your own account manager) forwards the RFP to a dedicated email address, Spojit pulls the PDF, and the intelligent layer drafts grounded answers using a workspace knowledge collection you have built from your best historical responses. A sales lead reviews and approves the draft in the Approvals inbox before a single word leaves the building, so the prospect only ever sees vetted language.
The run is push-driven and fully asynchronous. A Mailhook trigger fires within seconds of mail arriving (no mailbox or polling), an Attachment node fetches the PDF bytes, a Knowledge node in Query mode answers against a persistent collection plus a transient copy of this RFP, and a Human approval node pauses the workflow until a reviewer acts. On approval the workflow resumes and a Resend send-email call replies to {{ input.replyTo }}. If the reviewer rejects or the request times out, the run halts and nothing is sent. Each inbound message is deduplicated, so a forwarded-twice RFP will not double-answer. Re-running is safe: the persistent collection is read-only during a query and the transient copy is discarded when the run completes.
Prerequisites
- A persistent Knowledge collection (for example
sales-rfp-library) created in the Knowledge section of the sidebar, seeded with your past winning proposals, product datasheets, and security/compliance answers. Note the embedding model chosen at creation; you cannot change it later. - A Resend connection with a verified sending domain, so replies come from your own address. See the Resend connector overview and how to build email notifications with Resend.
- The email addresses (or roles/teams) of the sales lead(s) who will review drafts, so you can fill the approval slot.
- Optional: a Slack connection if you want to ping a channel when an RFP arrives. See the Slack connector overview.
- A draft RFP PDF you can use for testing.
Step 1: Seed the persistent knowledge collection
Open the Knowledge section in the sidebar and choose New Collection. Name it sales-rfp-library, add an optional description, and pick an embedding model (for example Gemini Embedding 001). Open the collection and use Upload Document to drag in your strongest past proposals, product datasheets, and a curated answer bank for common questions, then click Upload & Embed. Wait for each document to reach READY in the document table. This is the long-lived archive the workflow grounds its answers against, and any workflow in the workspace can query it. Re-uploading a file with the same name prompts you to Overwrite, Rename, or Cancel, so you can refresh a datasheet without creating duplicates.
Step 2: Receive the RFP with a Mailhook trigger
Create a new workflow, add a Trigger node, and set Trigger Type to Mailhook. Enter an Address prefix such as rfp (1 to 24 characters), click Generate email address, and copy the generated address (it looks like rfp-a1b2c3d4e5f6g7h8@mailhook.spojit.com). Share that address with your sales team or set a forwarding rule on your RFP intake inbox so questionnaires land there. To reduce noise, set an optional From allowlist and a Subject regex (for example (?i)rfp|proposal|questionnaire). The trigger output is available as {{ input }} with fields such as {{ input.subject }}, {{ input.from }}, {{ input.replyTo }}, {{ input.text }}, and an attachments list of references. For a deeper walk-through see setting up a Mailhook trigger and filtering Mailhook emails.
Step 3: Fetch the RFP PDF with an Attachment node
Add an Attachment node directly after the trigger (the designer only allows this node when the trigger is a Mailhook). Set Mode to Single so you get the first matching file as a single object, set the Content type filter to application/pdf, and set the Filename pattern to *.pdf. Turn on Fail if no attachment matches so an RFP body with no PDF stops the run cleanly instead of producing empty answers. Name the output variable, for example rfp_file. The node returns:
{
"filename": "acme-rfp-2026.pdf",
"contentType": "application/pdf",
"size": 184320,
"content": "JVBERi0xLjcKJ..." // base64
}
The base64 content feeds straight into the Knowledge node in the next step. Attachments are limited to 10 MB each and 25 MB per run by default, and received emails are retained for 30 days. For more detail see setting up email-triggered document processing.
Step 4: Embed the RFP into a transient collection
Add a Knowledge node in Embed mode. In the Collection dropdown choose Transient so this RFP is embedded into a throwaway, per-run collection that is auto-created, shared across nodes in the same run, and cleaned up when the run finishes. Set Document Type to PDF, set Document Input to {{ rfp_file.content }}, and name the Output Variable (for example rfp_embed). A transient collection needs no file name or embedding model. This step makes the RFP itself searchable so the answer step can pull the exact question text and any context tables from the document, while your persistent library supplies the proven answers.
Step 5: Answer the questions with a Query-mode Knowledge node and Response Schema
Add a second Knowledge node in Query mode. Set Collection to your persistent sales-rfp-library so answers are grounded in past proposals and product docs. In the Prompt, instruct the intelligent layer to read each question from the RFP and answer it from the library, and reference the transient material and trigger text so it has the questions in hand:
You are drafting responses to a sales RFP for our company.
The RFP questions are in this email and its attached PDF:
Subject: {{ input.subject }}
Body: {{ input.text }}
For each question in the RFP, write a clear, accurate answer using only
our proposal library and product documentation. If the library does not
cover a question, set "answer" to "" and add the question to "needs_input".
Do not invent capabilities or commitments.
Set Result Count higher than the default (for example 8) so longer questionnaires retrieve enough supporting passages, and pick a capable Model for synthesis. Turn on Response Schema to force structured, reviewable output instead of free text:
{
"type": "object",
"properties": {
"answers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"question": { "type": "string" },
"answer": { "type": "string" },
"confidence": { "type": "string", "enum": ["high", "medium", "low"] }
},
"required": ["question", "answer", "confidence"]
}
},
"needs_input": { "type": "array", "items": { "type": "string" } }
},
"required": ["answers", "needs_input"]
}
Name the Output Variable rfp_answers. Downstream you can reference {{ rfp_answers.answers }} and {{ rfp_answers.needs_input }}. For more on this pattern see querying your knowledge base and using structured output for reliable extraction.
Step 6: Gate the draft behind a Human approval node
Add a Human node so nothing reaches the prospect unreviewed. Set a Label like RFP answers ready for review and a Message that surfaces the key context, for example RFP "{{ input.subject }}" from {{ input.from }} has {{ rfp_answers.answers.length }} drafted answers and {{ rfp_answers.needs_input.length }} flagged for input. Set a Timeout (minutes) if you want unanswered requests to lapse (a timeout is treated as a rejection and halts the run). Fill the only required field, Approval slots, with an atom for your sales lead (a specific User, or a Role/Team). Approval completes only when every slot is satisfied. Optionally turn on Email approvers and raise Urgency for time-sensitive deals. Reviewers act in the Approvals inbox at /approvals; on approval the node outputs { approved: true, outcome: "APPROVED" } and the workflow continues. See reviewing and responding to approvals and the broader multi-step approval pattern.
Step 7: Email the approved answers back with Resend
After the Human node, add a Connector node on the Resend connector in Direct mode and pick the send-email tool. Map to to {{ input.replyTo }} (the address that sent the RFP, since Mailhook runs never reply to the sender automatically), set subject to something like Re: {{ input.subject }} - our responses, and leave from blank to use your connection default or set it to a verified address such as Sales <sales@yourdomain.com>. Build the body from the structured answers. If you prefer a clean HTML table, add a Transform node before this step to turn {{ rfp_answers.answers }} into HTML and pass it to the html field; otherwise loop the answers into the text field. A simple payload looks like:
{
"to": "{{ input.replyTo }}",
"subject": "Re: {{ input.subject }} - our responses",
"text": "Hello,\n\nPlease find our responses to your RFP attached below.\n\n{{ rfp_answers_text }}\n\nKind regards,\nThe Sales Team"
}
You can also attach the original RFP back for reference by adding an attachments entry with filename set to {{ rfp_file.filename }} and content set to {{ rfp_file.content }}. Because this step only runs after approval, the prospect always receives reviewed language.
Tips
- Use the SAME embedding model for the persistent library and for queries against it; mismatched models retrieve poorly. Choose it deliberately when you create
sales-rfp-library. - Keep the library curated. Embed your best, current answers and remove outdated datasheets so the intelligent layer never grounds a reply in stale pricing or deprecated features.
- Let
needs_inputdo its job: include it in the approval Message so the sales lead can fill gaps before sending, rather than discovering them after the prospect replies. - Add a Connector node on Slack with
send-messageright after the trigger to post "New RFP received" to a deals channel, so the team knows a review is incoming.
Common Pitfalls
- Do not try to reply with a Response node: Mailhook trigger runs are asynchronous and have no synchronous caller. Always reply with Resend (or a Send Email node) to
{{ input.replyTo }}. - An Attachment node cannot be saved without a Mailhook trigger, and if the RFP arrives as inline body text with no PDF, your
*.pdffilter matches nothing. Keep Fail if no attachment matches on, or add a Condition node to branch on body-only submissions. - Rejections and timeouts halt the run; there is no "on reject, do X" branch. If you need a revise-and-resubmit loop, have the reviewer reject, edit the source documents or prompt, and re-send the RFP to the Mailhook address.
- External recipients must satisfy your domain rules. With Resend, the reply comes from your verified sending domain; if you switch to a built-in Send Email node instead, the recipient must be on the org allowlist under Settings > General > Email recipients.
- Attachments over 10 MB each or 25 MB per run are rejected by default, so very large RFP packs may need to be split before forwarding.
Testing
Before pointing real prospects at the address, forward a sample RFP PDF to your generated Mailhook address from an allowlisted sender. Watch the run in execution history: confirm the Attachment node returns a non-empty base64 content, that rfp_answers.answers contains one entry per question with sensible confidence values, and that anything unanswered lands in needs_input. Approve the draft from the Approvals inbox and verify the Resend email arrives at the test sender with the right subject and body. Start with a short questionnaire and a small library, tune the Prompt and Result Count, then add a longer RFP once answers look reliable. Ask Miraxa, the intelligent layer across your automation, "Why did my last run fail?" if any step errors and it will read the run in context.