brainstorm · architecture overview

Proposal-Builder: Agent ↔ Twendee ERP

conversational 5-workflow flow on chat-ui + telegram · agent-runtime native

Sales rep chats freely across channels and days. The agent holds work-in-progress draft state in our Postgres; pushes the finalized proposal to Twendee ERP as a versioned ProposalDraft only when the rep attaches to a Deal. ERP also owns per-tenant config (rates, currency, T&C) via a new /settings/proposal-config endpoint.

Who lives where

Channels feed messages into a single API path. The proposal-builder skill mediates between our durable draft store and ERP, with file blobs in a separate store and a manager-approval channel via ConfirmationLedger.

Ctrl/Cmd + wheel = zoom · drag = pan · double-click = fit

Loading...
agent-owned state
ERP-owned state
blob storage
async / HITL
Why agent owns mid-flight, ERP owns final. Twendee's addProposalDraft bumps a version each call — pushing on every W1–W4 save would create noisy versions. So the agent holds working state until W5 finalize, then pushes a single coherent draft. Subsequent revisions (post-feedback) push as new versions.

What goes where

Hard line between agent-side state (volatile, conversational, attachment-heavy) and ERP-side records (canonical business entities, versioned artifacts, tenant policy).

Concern Our Postgres (agent) Twendee ERP
WIP per-phase content proposal_drafts.requirements_json
scope_json · technical_json · cost_json
Workflow phase & version proposal_drafts.phase enum
.version (optimistic lock)
Owners / share access proposal_drafts.owners[] (rep + invited co-owners/manager)
Chat session → draft binding chat_sessions.active_proposal_draft_id (nullable fk)
Conversation history messages (existing)
RFP / brief uploads attachments + blob in our store, signed URL (no native file storage)
Final rendered docs proposal_drafts.final_md / .final_docx_url / .final_pdf_url ProposalDraft.pdfUrl (reference only)
Company / Deal (canonical) Companies, Deals
Published proposal versions deals.addProposalDraft() · tagged JSON in deal comment · auto-inc version
Tenant config (rates, T&C, currency, approvers) 5-min in-memory cache only GET /settings/proposal-config (new this sprint)
Manager approval flow approval_state + ConfirmationLedger pause/resume ProposalDraft.status moves: pending_rep_reviewapproval_requiredsent

A proposal across a week

One sales rep, multiple chat sessions (chat-ui Mon → Telegram Tue → fresh chat-ui Wed), one durable proposal_drafts row, one ERP push at finalize.

Ctrl/Cmd + wheel = zoom · drag = pan

Loading...
Cross-channel resume. Because draft state is detached from chat session (proposal_drafts is independent; chat_sessions.active_proposal_draft_id is a pointer), the rep can continue on Telegram, in a new chat-ui session, or even pass to a co-owner via shareProposal(). First turn of a new session detects intent and calls resumeProposal().