Build a Deal-Flow Agent
Use MindGraph as the governed organizational brain behind an investment workflow: typed companies and leads, source-backed diligence, versioned score inputs, deterministic rankings, and an audit trail your application can explain.
This guide builds the knowledge and analysis layer, not a replacement CRM. Your product should continue to own its pipeline UI, inbox, tasks, notifications, permissions UX, and external integrations.
The system boundary
Your deal-flow application owns
- Pipeline, inbox, tasks, reminders, and collaboration UI
- Email, CRM, data-room, market-data, and cap-table integrations
- Economic models, portfolio construction, and final IC workflow
MindGraph owns
- Typed domain objects, relations, identity, and history
- Documents, extracted claims, evidence, risks, and provenance
- Governed agent access and deterministic comparative analysis
1. Connect your agent
npm install mindgraph
import { MindGraph } from "mindgraph";
const graph = new MindGraph({
baseUrl: process.env.MINDGRAPH_URL ?? "https://api.mindgraph.cloud",
apiKey: process.env.MINDGRAPH_API_KEY!,
});
create_domain_object(), update_domain_object(), ingest_document(), and query_domain_structured(). See the SDK guide for setup.Create separate API keys or agent identities for ingestion, analyst automation, and production reads. Grant each only the spaces and actions it needs. See Governance & Access.
2. Establish the deal-flow contract
Ask MindGraph to draft the built-in investment contract, review the proposal, then activate it. The template defines Company, Contact, FundThesis, and InvestmentLead, plus the relations between them.
const { schema_id } = await graph.proposeOntologySchema({
template_hint: "investment_dealflow",
target_use_case: "Govern our venture pipeline and diligence evidence",
desired_workflows: [
"Screen every admitted lead against the fund thesis",
"Compare complete scorecards without hiding missing candidates",
"Trace every investment claim to its source",
],
});
// Poll the draft, review the proposed schema, then activate it.
let schema = await graph.getOntologySchema(schema_id);
while (schema.propose_status === "pending" || schema.propose_status === "running") {
await new Promise((resolve) => setTimeout(resolve, 1_000));
schema = await graph.getOntologySchema(schema_id);
}
if (schema.propose_status !== "ready") {
throw new Error(schema.propose_error ?? "Schema proposal failed");
}
// Present schema.object_types and relation_types for a human review here.
await graph.activateOntologySchema(schema_id);
InvestmentLead.deal_id is the stable identity supplied by your application. It is intentionally separate from the company: the same company may return in another round or process. Never derive it from a name, document wording, or model confidence.stage and round enum values to your stated workflow. Read the activated schema and use its returned enum values rather than hard-coding values from this example.3. Create leads from your source application
Create the stable record before ingesting pitch materials. Manual creates are checked against the active schema: required fields, enum values, number types, references, and identity must all be valid.
const company = await graph.createDomainObject({
schema_id,
object_type: "Company",
canonical_name: "Acme Robotics",
fields: { name: "Acme Robotics" },
});
const lead = await graph.createDomainObject({
schema_id,
object_type: "InvestmentLead",
canonical_name: "Acme Robotics — Series A",
fields: {
deal_id: "crm-deal-1042", // stable ID from your application
company: company.uid, // validated Company reference
stage: "screening", // use an enum value from the active schema
round: "series_a",
received_on: "2026-08-18",
},
});
await graph.linkDomainObjects({
from_uid: lead.uid,
to_uid: company.uid,
relation_type: "LEAD_FOR",
});
Use INTRODUCED_BY to connect a lead to one or more contacts and MATCHES_THESIS to connect it to the relevant fund thesis. Relations are typed and direction-sensitive, so follow the source and target types in the schema.
4. Update score inputs safely
Authored updates use optimistic concurrency. Read the current version, submit only the changed fields, and record a human-readable reason. Identity fields are immutable; explicit unset_fields removes optional values without treating null as a valid domain value.
const current = await graph.getDomainObject(lead.uid, {
schema_id,
object_type: "InvestmentLead",
});
const updated = await graph.updateDomainObject(lead.uid, {
fields: {
stage: "diligence",
team_score: 8,
market_score: 7.5,
thesis_fit_score: 9,
},
unset_fields: ["risk_score"],
expected_version: current.version,
reason: "Partner screening review completed on 2026-08-18",
});
console.log(updated.uid, updated.version, updated.proposal_id);
If another writer changed the object first, the API returns a conflict. Fetch the current object, show the competing version to the user, and retry only after reconciling it. Do not silently overwrite the newer assessment.
5. Attach source-backed diligence
Ingest decks, notes, memos, transcripts, and data-room exports into a project-scoped corpus. Include the external deal ID in the ingestion context so extraction can resolve the pre-created lead rather than inventing a new identity.
const ingestion = await graph.ingestDocument({
title: "Acme Robotics — partner meeting notes",
content: meetingNotes,
content_type: "meeting_notes",
source_uri: "crm://deals/crm-deal-1042/notes/partner-meeting",
project_uid: "project_acme_diligence",
ontology_schema_id: schema_id,
layers: ["reality", "epistemic", "intent", "action", "ontology"],
context: "InvestmentLead deal_id=crm-deal-1042; Company=Acme Robotics",
});
let job = await graph.getJob(ingestion.job_id);
while (job.status === "pending" || job.status === "processing") {
await new Promise((resolve) => setTimeout(resolve, 1_000));
job = await graph.getJob(ingestion.job_id);
}
if (job.status !== "completed") throw new Error("Diligence ingestion failed");
const { items } = await graph.listOntologyProposals({
schema_id,
extract_job_id: ingestion.job_id,
status: "pending",
});
// Render items in your authenticated analyst review surface. Populate this only
// with IDs explicitly selected there; an empty list approves nothing.
const selectedProposalIds: string[] = [];
for (const proposalId of selectedProposalIds) {
await graph.approveOntologyProposal(proposalId);
}
getDomainObjectContext() when the agent needs both.6. Keep financial history as time series
Do not flatten changing metrics such as ARR, burn, or headcount into an ever-growing object. Store dense measurements as Series nodes associated with the company or lead.
const { series: arr } = await graph.createSeries({
entity_uid: company.uid,
name: "annual-recurring-revenue",
description: "Company-reported ARR",
unit: "USD",
temporality: "period",
period_unit: "month",
default_source_uid: ingestion.document_uid,
});
await graph.appendSeries({
series_uid: arr.uid,
points: [{
t: 1785542400000000, // UTC microseconds
value: 4_200_000,
period_label: "2026-08",
source_uid: ingestion.document_uid,
}],
});
const latestArr = await graph.latestSeries(arr.uid, {
project_uid: "project_acme_diligence",
});
7. Rank admitted leads deterministically
The weighted scorecard reads authored numeric fields, normalizes each criterion over the admitted candidate set, applies explicit directions and weights, and returns component calculations. Model confidence and retrieval similarity never enter the score.
const comparison = await graph.queryDomainStructured({
schema_id,
select: "InvestmentLead",
where: [{ field: "stage", op: "in", value: ["screening", "diligence"] }],
aggregate: {
op: "weighted_scorecard",
criteria: [
{ field: "team_score", weight: 3, direction: "higher_is_better" },
{ field: "market_score", weight: 2, direction: "higher_is_better" },
{ field: "thesis_fit_score", weight: 3, direction: "higher_is_better" },
{ field: "risk_score", weight: 2, direction: "lower_is_better" },
],
missing: "exclude_candidate",
},
});
const scorecard = comparison.aggregate as {
ranking: Array<{
object_uid: string;
score: number;
rank: number;
tied: boolean;
tie_group_size: number;
components: unknown[];
}>;
ties: Array<{ rank: number; score: number; object_uids: string[] }>;
excluded_candidates: Array<{ object_uid: string; missing_fields: string[] }>;
};
Show excluded_candidates next to the ranking so incomplete leads do not disappear. Preserve equal scores as ties: each ranked item includes rank, tied, and tie_group_size, and the aggregate includes explicit tie groups.
8. Put it into an agent loop
- Receive a deal event from the external CRM or inbox.
- Create or resolve the stable Company and InvestmentLead.
- Ingest new materials into the deal's scoped project and wait for completion.
- Present uncertain extracted objects and relations for human approval.
- Retrieve the lead with cognitive context and provenance for analysis.
- Write reviewed score inputs with the current version and an audit reason.
- Run the deterministic scorecard over an explicit admitted candidate set.
- Return rankings, missing data, ties, evidence, and risks to your product UI.
Production checklist
- Keep a durable external
deal_id → MindGraph uidmapping. - Read schema enums and required fields at startup; fail closed on contract drift.
- Retry version conflicts only after reconciling the competing update.
- Poll every asynchronous job to a terminal state and surface failures.
- Use project scope for deal-specific retrieval and least-privilege agent access.
- Never turn extraction confidence into an investment score.
- Show provenance, missing score inputs, tie groups, and candidate coverage.
- Record risk and final IC decisions separately from deterministic ranking.
Continue with the Operational Ontology guide for schema details, the Projects & Living Briefs guide for bounded diligence corpora, and the API Reference for endpoint-level contracts.