FN-6759: add built-in lead-generation workflow

Add a selectable business workflow for managing lead-generation tasks.

- Register a built-in lead-generation workflow with custom business columns, lead fields, and stage prompts.
- Cover workflow registration, column traits, field metadata, compilation, and IR round-tripping with core tests.
- Document the workflow catalog entry and add a minor changeset for the published CLI package.

Files changed:
 .changeset/fn-6759-lead-generation-workflow.md     |   5 +
 docs/workflow-steps.md                             |   4 +
 .../builtin-lead-generation-workflow-ir.test.ts    | 123 ++++++++++++++++++
 .../core/src/__tests__/builtin-workflows.test.ts   |  10 ++
 .../src/builtin-lead-generation-workflow-ir.ts     | 141 +++++++++++++++++++++
 packages/core/src/builtin-workflows.ts             |  20 +++
 packages/core/src/index.ts                         |   1 +
 7 files changed, 304 insertions(+)

Fusion-Task-Id: FN-6759

Fusion-Task-Lineage: 6c724f84-a6a6-4b01-93ec-cabb9387be2f
This commit is contained in:
gsxdsm
2026-06-20 08:43:35 -07:00
parent 281ce35829
commit 52924ba23d
7 changed files with 304 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": minor
---
Add a built-in lead-generation workflow with custom lead columns, fields, and stage prompts.

View File

@@ -9,6 +9,9 @@ Workflow steps are reusable quality gates that run around task completion.
<!--
FNXC:Docs 2026-06-16-23:25:
Public docs need one concise workflow overview that names the shipped built-ins, explains per-task selection, and points authors to the visual editor while leaving low-level runtime details in this canonical workflow document.
FNXC:Docs 2026-06-20-08:47:
The built-in catalog now includes a business lead-generation workflow with custom columns, custom lead fields, and inline per-stage prompts, so the public inventory must show it beside coding workflows instead of implying all selectable built-ins are engineering-only.
-->
Fusion workflows define the task lifecycle policy that moves work from an idea to delivery. The default coding path is **Plan/Triage → Execute → Workflow steps → Review → Merge**, but that path is now represented as a workflow selection rather than only as fixed engine behavior. A task with no explicit workflow resolves to `builtin:coding`; an explicit missing/corrupt custom workflow fails closed instead of silently falling back.
@@ -35,6 +38,7 @@ Decision-only or investigation tasks can also declare `noCommitsExpected` / `**N
| Stepwise coding | `builtin:stepwise-coding` | Graph-executor workflow that models per-step parse/execute/review/rework explicitly. |
| Design | `builtin:design` | UI-heavy work path that implements, runs a gated design/UX review, then performs the standard review and merge. |
| PR lifecycle | `builtin:pr-workflow` | Reusable PR lifecycle graph fragment (create PR → await review → respond → gate → merge); it is a fragment, not directly selectable as a task workflow. |
| Lead generation | `builtin:lead-generation` | Selectable business workflow for sourcing, qualifying, enriching, and contacting leads with custom lead fields and stage columns; requires the workflow graph executor for custom board columns. |
### Custom workflow authoring

View File

@@ -0,0 +1,123 @@
import { describe, expect, it } from "vitest";
import { BUILTIN_LEAD_GENERATION_WORKFLOW_IR } from "../builtin-lead-generation-workflow-ir.js";
import {
BUILTIN_WORKFLOWS,
defaultEnabledBuiltinWorkflowIds,
getBuiltinWorkflow,
} from "../builtin-workflows.js";
import { compileWorkflowToSteps } from "../workflow-compiler.js";
import { parseWorkflowIr, serializeWorkflowIr } from "../workflow-ir.js";
describe("built-in lead-generation workflow IR", () => {
it("registers as an enabled v2 workflow with the authored custom columns", () => {
const workflow = getBuiltinWorkflow("builtin:lead-generation");
expect(workflow).toBeDefined();
expect(workflow!.kind).toBe("workflow");
expect(workflow!.ir).toBe(BUILTIN_LEAD_GENERATION_WORKFLOW_IR);
expect(BUILTIN_WORKFLOWS.some((candidate) => candidate.id === "builtin:lead-generation")).toBe(true);
expect(defaultEnabledBuiltinWorkflowIds()).toContain("builtin:lead-generation");
const ir = parseWorkflowIr(workflow!.ir);
expect(ir.version).toBe("v2");
if (ir.version !== "v2") throw new Error("expected v2");
expect(ir.columns.map((column) => column.id)).toEqual([
"triage",
"sourcing",
"qualification",
"enrichment",
"outreach",
"converted",
"archived",
]);
expect(ir.columns.map((column) => column.traits.map((trait) => trait.trait))).toEqual([
["intake"],
["timing"],
["wip", "timing"],
["timing"],
["human-review", "stall-detection"],
["complete"],
["archived"],
]);
expect(ir.columns.filter((column) => column.traits.some((trait) => trait.trait === "intake"))).toHaveLength(1);
expect(ir.columns.filter((column) => column.traits.some((trait) => trait.trait === "complete"))).toHaveLength(1);
expect(ir.columns.filter((column) => column.traits.some((trait) => trait.trait === "archived"))).toHaveLength(1);
});
it("places every node in a defined column and compiles the linear prompt spine", () => {
const workflow = getBuiltinWorkflow("builtin:lead-generation")!;
const ir = parseWorkflowIr(workflow.ir);
if (ir.version !== "v2") throw new Error("expected v2");
const columnIds = new Set(ir.columns.map((column) => column.id));
for (const node of ir.nodes) {
expect(node.column, node.id).toBeDefined();
expect(columnIds.has(node.column!), node.id).toBe(true);
}
expect(ir.nodes.filter((node) => node.kind === "start")).toHaveLength(1);
expect(ir.nodes.filter((node) => node.kind === "end")).toHaveLength(1);
expect(ir.nodes.find((node) => node.id === "start")?.column).toBe("triage");
expect(ir.nodes.find((node) => node.id === "end")?.column).toBe("converted");
expect(ir.nodes.find((node) => node.id === "qualification-gate")?.kind).toBe("gate");
expect((ir.nodes.find((node) => node.id === "qualification-gate")?.config as { gateMode?: string })?.gateMode).toBe(
"advisory",
);
for (const node of ir.nodes.filter((candidate) => candidate.kind === "prompt" || candidate.kind === "gate")) {
const config = node.config as { prompt?: string; seam?: string } | undefined;
expect(config?.seam, node.id).toBeUndefined();
expect(config?.prompt, node.id).toEqual(expect.stringMatching(/lead|prospect|outreach|customer|company/i));
}
expect(compileWorkflowToSteps(ir).map((step) => step.name)).toEqual([
"Source prospects",
"Qualify lead",
"Qualification go / no-go",
"Enrich lead",
"Draft and send outreach",
]);
});
it("declares lead fields with expected types and enum options", () => {
const workflow = getBuiltinWorkflow("builtin:lead-generation")!;
const ir = parseWorkflowIr(workflow.ir);
if (ir.version !== "v2") throw new Error("expected v2");
expect(ir.fields?.map((field) => [field.id, field.type])).toEqual([
["company", "string"],
["contactName", "string"],
["contactEmail", "url"],
["leadSource", "enum"],
["leadScore", "number"],
["leadStatus", "enum"],
]);
const fields = new Map(ir.fields?.map((field) => [field.id, field]));
expect(fields.get("leadSource")?.options?.map((option) => option.value)).toEqual([
"referral",
"inbound",
"outbound",
"event",
"partner",
]);
expect(fields.get("leadStatus")?.options?.map((option) => option.value)).toEqual([
"new",
"qualified",
"contacted",
"responded",
"won",
"lost",
]);
expect(fields.get("company")?.render).toEqual({ placement: "card", widget: "input" });
expect(fields.get("leadStatus")?.render).toEqual({ placement: "card", widget: "select" });
});
it("round-trips through serialize → parse unchanged", () => {
const workflow = getBuiltinWorkflow("builtin:lead-generation")!;
const serialized = serializeWorkflowIr(workflow.ir);
const reparsed = parseWorkflowIr(serialized);
expect(serializeWorkflowIr(reparsed)).toBe(serialized);
});
});

View File

@@ -95,6 +95,16 @@ describe("built-in workflows", () => {
expect(serializeWorkflowIr(reparsed)).toBe(serialized);
});
it("includes the lead-generation built-in after existing built-ins without disturbing default order", () => {
const leadGeneration = getBuiltinWorkflow("builtin:lead-generation");
expect(leadGeneration).toBeDefined();
expect(leadGeneration!.kind).toBe("workflow");
expect(defaultEnabledBuiltinWorkflowIds()).toContain("builtin:lead-generation");
expect(BUILTIN_WORKFLOWS.findIndex((workflow) => workflow.id === "builtin:lead-generation")).toBeGreaterThan(
BUILTIN_WORKFLOWS.findIndex((workflow) => workflow.id === "builtin:pr-workflow"),
);
});
it("default workflow column ids equal the legacy enum values, in legacy order (KTD-1)", () => {
expect(BUILTIN_CODING_WORKFLOW_IR.version).toBe("v2");
if (BUILTIN_CODING_WORKFLOW_IR.version !== "v2") throw new Error("expected v2");

View File

@@ -0,0 +1,141 @@
import type { WorkflowIr } from "./workflow-ir-types.js";
import { parseWorkflowIr } from "./workflow-ir.js";
import { BUILTIN_WORKFLOW_SETTINGS } from "./builtin-workflow-settings.js";
/**
* FNXC:Workflows 2026-06-20-00:25:
* The lead-generation built-in must be a first-class v2 workflow with its own business pipeline columns, lead-specific task fields, and inline per-stage prompts instead of coding seams.
* The custom non-default column ids make this workflow graph-executor-oriented at runtime while still compiling its linear prompt/gate spine for legacy step materialization.
*/
const RAW_BUILTIN_LEAD_GENERATION_WORKFLOW_IR: WorkflowIr = {
version: "v2",
name: "builtin-lead-generation",
columns: [
{ id: "triage", name: "Lead intake", traits: [{ trait: "intake" }] },
{ id: "sourcing", name: "Sourcing", traits: [{ trait: "timing" }] },
{
id: "qualification",
name: "Qualification",
traits: [
{ trait: "wip", config: { limitSetting: "maxConcurrent", countPending: true } },
{ trait: "timing" },
],
},
{ id: "enrichment", name: "Enrichment", traits: [{ trait: "timing" }] },
{
id: "outreach",
name: "Outreach",
traits: [
{ trait: "human-review" },
{ trait: "stall-detection", config: { timeoutMs: 86_400_000, action: "notify" } },
],
},
{ id: "converted", name: "Converted", traits: [{ trait: "complete" }] },
{ id: "archived", name: "Archived", traits: [{ trait: "archived" }] },
],
fields: [
{ id: "company", name: "Company", type: "string", render: { placement: "card", widget: "input" } },
{ id: "contactName", name: "Contact name", type: "string", render: { placement: "detail", widget: "input" } },
{ id: "contactEmail", name: "Contact email", type: "url", render: { placement: "detail", widget: "input" } },
{
id: "leadSource",
name: "Lead source",
type: "enum",
options: [
{ value: "referral", label: "Referral" },
{ value: "inbound", label: "Inbound" },
{ value: "outbound", label: "Outbound" },
{ value: "event", label: "Event" },
{ value: "partner", label: "Partner" },
],
render: { placement: "card", widget: "select" },
},
{ id: "leadScore", name: "Lead score", type: "number", render: { placement: "card", widget: "input" } },
{
id: "leadStatus",
name: "Lead status",
type: "enum",
options: [
{ value: "new", label: "New" },
{ value: "qualified", label: "Qualified" },
{ value: "contacted", label: "Contacted" },
{ value: "responded", label: "Responded" },
{ value: "won", label: "Won" },
{ value: "lost", label: "Lost" },
],
render: { placement: "card", widget: "select" },
},
],
nodes: [
{ id: "start", kind: "start", column: "triage" },
{
id: "source-prospects",
kind: "prompt",
column: "sourcing",
config: {
name: "Source prospects",
executor: "model",
prompt:
"Research and identify promising prospects for this lead-generation task. Capture target companies, likely buyer personas, trigger events, and the evidence behind each prospect so downstream qualification can judge fit.",
},
},
{
id: "qualify-lead",
kind: "prompt",
column: "qualification",
config: {
name: "Qualify lead",
executor: "model",
prompt:
"Evaluate each sourced prospect against the ideal customer profile. Score company fit, pain urgency, budget or buying signals, and disqualifying risks; update lead score and recommend qualified or lost status with concise rationale.",
},
},
{
id: "qualification-gate",
kind: "gate",
column: "qualification",
config: {
name: "Qualification go / no-go",
gateMode: "advisory",
prompt:
"Advisory check: decide whether this lead should continue to enrichment. Continue for plausible fit, but record any concerns, missing data, or reasons the lead may be low priority.",
},
},
{
id: "enrich-lead",
kind: "prompt",
column: "enrichment",
config: {
name: "Enrich lead",
executor: "model",
prompt:
"Enrich the qualified lead with company context and contact data. Add verified company details, relevant news or initiatives, likely stakeholders, contact name, contact email or profile URL, and personalization hooks for outreach.",
},
},
{
id: "draft-outreach",
kind: "prompt",
column: "outreach",
config: {
name: "Draft and send outreach",
executor: "model",
prompt:
"Draft concise personalized outreach for the enriched lead. Reference the strongest trigger or pain evidence, state the proposed value clearly, choose a low-friction call to action, and record the sent or ready-to-send message plus follow-up timing.",
},
},
{ id: "end", kind: "end", column: "converted" },
],
edges: [
{ from: "start", to: "source-prospects" },
{ from: "source-prospects", to: "qualify-lead", condition: "success" },
{ from: "qualify-lead", to: "qualification-gate", condition: "success" },
{ from: "qualification-gate", to: "enrich-lead", condition: "success" },
{ from: "enrich-lead", to: "draft-outreach", condition: "success" },
{ from: "draft-outreach", to: "end", condition: "success" },
],
settings: BUILTIN_WORKFLOW_SETTINGS,
};
export const BUILTIN_LEAD_GENERATION_WORKFLOW_IR = parseWorkflowIr(
RAW_BUILTIN_LEAD_GENERATION_WORKFLOW_IR,
);

View File

@@ -1,4 +1,5 @@
import { BUILTIN_CODING_WORKFLOW_IR } from "./builtin-coding-workflow-ir.js";
import { BUILTIN_LEAD_GENERATION_WORKFLOW_IR } from "./builtin-lead-generation-workflow-ir.js";
import { BUILTIN_MARKETING_WORKFLOW_IR } from "./builtin-marketing-workflow-ir.js";
import { BUILTIN_PR_WORKFLOW_IR } from "./builtin-pr-workflow-ir.js";
import { BUILTIN_STEPWISE_CODING_WORKFLOW_IR } from "./builtin-stepwise-coding-workflow-ir.js";
@@ -343,6 +344,25 @@ export const BUILTIN_WORKFLOWS: WorkflowDefinition[] = [
createdAt: BUILTIN_TS,
updatedAt: BUILTIN_TS,
},
{
id: "builtin:lead-generation",
name: "Lead generation (built-in)",
description:
"A business pipeline for sourcing, qualifying, enriching, and contacting leads with custom lead fields and stage columns. Requires the workflow graph executor for custom board columns.",
kind: "workflow",
ir: BUILTIN_LEAD_GENERATION_WORKFLOW_IR,
layout: {
start: { x: 60, y: 160 },
"source-prospects": { x: 230, y: 160 },
"qualify-lead": { x: 400, y: 160 },
"qualification-gate": { x: 570, y: 160 },
"enrich-lead": { x: 740, y: 160 },
"draft-outreach": { x: 910, y: 160 },
end: { x: 1080, y: 160 },
},
createdAt: BUILTIN_TS,
updatedAt: BUILTIN_TS,
},
];
const BUILTIN_BY_ID = new Map(BUILTIN_WORKFLOWS.map((wf) => [wf.id, wf]));

View File

@@ -122,6 +122,7 @@ export { resolveWorkflowOptionalSteps } from "./workflow-optional-steps.js";
export type { ResolvedWorkflowOptionalStep } from "./workflow-optional-steps.js";
export { BUILTIN_STEPWISE_CODING_WORKFLOW_IR } from "./builtin-stepwise-coding-workflow-ir.js";
export { BUILTIN_PR_WORKFLOW_IR } from "./builtin-pr-workflow-ir.js";
export { BUILTIN_LEAD_GENERATION_WORKFLOW_IR } from "./builtin-lead-generation-workflow-ir.js";
export {
BUILTIN_WORKFLOW_SETTINGS,
BUILTIN_MOVED_WORKFLOW_SETTINGS,