RFP handling
Read inbound RFP documents from a central Sales folder, extract structured metadata into DataPool, resolve the customer from CRM (master), draft an offer as a tenant file, and require human approval before any customer send.
Document information
| Field | Value |
|---|---|
| Canonical URL | /docs/07_ai-agents-and-mcp/model_skills/20_sales/rfp-handling |
| Version (published date) | 2026-09-01 |
| Tags | ai, skills, sales, rfp, datapool, playbooks |
Kind
Sequential procedure.
Depends on
tenant-data-model, crm-field-mapping. Optional: sales-process (link or create opportunity_id at proposal stage).
Prerequisites
| id | question | search | if_missing_or_ambiguous | default_to_propose | binds_to |
|---|---|---|---|---|---|
rfp_requests_schema | DataPool schema for RFP metadata | tenant-data-model | Import tenant-data-model first | rfp_requests | Tenant SKILL.md |
incoming_rfp_folder | Folder for inbound RFP files | list_directory under tenantdata Sales/ | Ask; create folders if missing | Sales/Incoming-RFPs | Tenant SKILL.md |
offers_folder | Folder for draft offer files | list_directory Sales/ | Ask | Sales/Offers | Tenant SKILL.md |
crm_integration_id | CRM integration (customer master) | crm-field-mapping | Import and bind CRM | None | Tenant SKILL.md |
opportunities_schema | DataPool opportunities (optional link) | tenant-data-model | Import first | opportunities | Tenant SKILL.md |
sales_approver_user_id | Tenant user who approves offers before send | entity_search users / ask | request_human_input | None | Tenant SKILL.md |
mail_integration_id | Mail integration for Trace-assisted send (optional) | list_integrations Gmail / Outlook / IMAP | Ask; leave empty if operator sends manually | None | Tenant SKILL.md |
Import contract
| Field | Value |
|---|---|
suggested_skill_id | rfp-handling |
version | 1.0.0 |
capabilities.tools | list_directory, read_file, write_file, pdf_extract_text, describe_datapool_table, query_datapool, insert_datapool_rows, update_datapool_rows, describe_tenant_integration, execute_tenant_integration, request_human_input, list_integrations |
| Paired model agent | incoming-rfp-watcher |
Procedure (generic B2B)
Source of truth split
| Data | Master | Notes |
|---|---|---|
| Customer account, contacts, deal history | CRM ({{crm_integration_id}}) | Never invent account ids; resolve via CRM search |
| RFP intake metadata, status, file paths | DataPool {{rfp_requests_schema}} | Pointers only — not a second CRM |
| Draft offer body | File under {{offers_folder}}/{rfp_id}/ | Markdown or PDF; path stored on the row |
Statuses: new → extracted → drafting → pending_approval → approved → sent (or declined at any gate).
| Stage | Quality gate to exit |
|---|---|
| new | Inbound file listed under {{incoming_rfp_folder}}; row exists with source_file_path |
| extracted | Required fields in requirements_summary (see DoD); CRM account resolved or explicitly crm_account_id=unknown with operator note |
| drafting | Draft file written; draft_offer_path set |
| pending_approval | request_human_input / operator notified; no customer send |
| approved | Approver confirmed; approved_by_user_id and approved_at set |
| sent | Send confirmed; sent_at set — only after approved |
DoD — extracted (requirements_summary JSON text): at minimum customer_name, submission_deadline (or deadline_unknown=true), scope_summary, evaluation_criteria (array or string), mandatory_requirements (array or string). Optional: budget_hint, contact_email, reference_number.
DoD — draft offer file: sections for executive summary, scope response, commercial outline (line items or TBD with reason), assumptions, exclusions, validity period. Do not state final prices as committed until approved.
Send rules (mandatory human gate):
- Never email the customer without explicit operator confirmation in the same session.
- Trace:
request_human_inputwith choices — Send via mail integration (only if{{mail_integration_id}}is bound), I will send manually, or Cancel. - On manual send: operator marks
status=sentafter they sent outside Tealfabric. - On Trace send: use
execute_tenant_integrationon the bound mail integration; attach or body fromdraft_offer_path; then setstatus=sentandsent_at. - Autonomic agents: use decision
need_humanatpending_approvaland before any send act — noexecute_tenant_integrationfor outbound customer mail unless the operator resumed with explicit send approval.
Tenant SKILL.md (copy this body)
# RFP handling
Inbound folder: `{{incoming_rfp_folder}}`. Draft offers folder: `{{offers_folder}}`. DataPool schema: `{{rfp_requests_schema}}`.
CRM is the **customer master** (`{{crm_integration_id}}`). Load `integrations`, tenant skills `tenant-data-model` and `crm-field-mapping`, and platform skill `datapool`.
Sales approver user id: `{{sales_approver_user_id}}`. Optional mail integration for assisted send: `{{mail_integration_id}}` (empty = manual send only).
## Intake and extract
1. `list_directory` `{{incoming_rfp_folder}}` (non-recursive unless operator bound otherwise).
2. For each file without a matching `source_file_path` row in `{{rfp_requests_schema}}`:
- `read_file` or `pdf_extract_text` as appropriate.
- Insert row: `rfp_id` (new uuid), `source_file_path`, `status=new`, `owner_user_id` = approver if known.
3. Parse the document into `requirements_summary` JSON (DoD fields). Set `status=extracted`.
4. Resolve customer in CRM (`describe_tenant_integration`, search by company name). Set `crm_account_id` when found. Set `company_name` from CRM when it differs from the RFP header. Do **not** overwrite CRM records from RFP text alone.
5. Optionally link `opportunity_id` in `{{opportunities_schema}}` at `stage=proposal` (see `sales-process`).
## Draft offer
1. Create `{{offers_folder}}/{rfp_id}/offer.md` (or operator-approved extension) with `write_file`.
2. Set `draft_offer_path`, `status=drafting`, then `status=pending_approval`.
3. `request_human_input`: show `rfp_id`, `company_name`, deadline, path to draft, and summary. Ask approver to review the file and choose **Approve**, **Revise**, or **Decline**.
## Approval
- **Approve:** set `status=approved`, `approved_by_user_id`, `approved_at`. Do not send yet.
- **Revise:** keep or return to `drafting`; update file and re-enter `pending_approval`.
- **Decline:** set `status=declined` with reason in `requirements_summary` or a notes field if bound.
## Send (after approved only)
1. `request_human_input`: confirm recipient email (from CRM contact or RFP), subject, and send mode.
2. If operator chooses manual send: instruct them to send from their mail client; after confirmation set `status=sent`, `sent_at`.
3. If operator chooses Trace send and `{{mail_integration_id}}` is set: `execute_tenant_integration` with draft content; on success set `status=sent`, `sent_at`.
4. Never send when `status` is not `approved`.
## Stop
- Do not invent CRM account or contact ids.
- Do not copy full CRM payloads into DataPool — store ids and extracted RFP facts only.
- Do not skip `pending_approval` or send without confirmation.
- Do not delete inbound RFP files without operator confirmation.
Optional skill.json
{
"name": "rfp-handling",
"version": "1.0.0",
"description": "Ingest RFPs from Sales storage, extract metadata, draft offers, human approval before customer send.",
"capabilities": {
"prompt": [
"CRM is customer master. Draft offers are files; metadata in DataPool.",
"Never send to customer without explicit human approval."
],
"resources": [],
"tools": [
"list_directory",
"read_file",
"write_file",
"pdf_extract_text",
"describe_datapool_table",
"query_datapool",
"insert_datapool_rows",
"update_datapool_rows",
"describe_tenant_integration",
"execute_tenant_integration",
"list_integrations",
"request_human_input"
]
}
}