From 281ce358291f8297ca1e1b39ac0f813e1186e367 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 20 Jun 2026 08:20:39 -0700 Subject: [PATCH] FN-6761: add built-in marketing workflow Add a content-focused workflow while preserving standard Fusion lifecycle and merge behavior. - Register builtin:marketing in the built-in workflow catalog and default-enabled workflow order. - Define marketing-specific columns, content brief/draft/editorial prompts, lifecycle traits, and merge primitives. - Cover the marketing workflow in built-in workflow tests and document it in workflow-step docs. - Add a minor changeset for the published Fusion package. Files changed: .changeset/fn-6761-marketing-workflow.md | 5 + docs/workflow-steps.md | 3 + .../core/src/__tests__/builtin-workflows.test.ts | 64 +++++++++++- packages/core/src/builtin-marketing-workflow-ir.ts | 116 +++++++++++++++++++++ packages/core/src/builtin-workflows.ts | 24 +++++ packages/core/src/index.ts | 1 + 6 files changed, 209 insertions(+), 4 deletions(-) Fusion-Task-Id: FN-6761 Fusion-Task-Lineage: 3242eceb-570f-47f8-8750-dc3acc971bbd --- .changeset/fn-6761-marketing-workflow.md | 5 + docs/workflow-steps.md | 3 + .../src/__tests__/builtin-workflows.test.ts | 64 +++++++++- .../core/src/builtin-marketing-workflow-ir.ts | 116 ++++++++++++++++++ packages/core/src/builtin-workflows.ts | 24 ++++ packages/core/src/index.ts | 1 + 6 files changed, 209 insertions(+), 4 deletions(-) create mode 100644 .changeset/fn-6761-marketing-workflow.md create mode 100644 packages/core/src/builtin-marketing-workflow-ir.ts diff --git a/.changeset/fn-6761-marketing-workflow.md b/.changeset/fn-6761-marketing-workflow.md new file mode 100644 index 0000000000..fec6fd79cd --- /dev/null +++ b/.changeset/fn-6761-marketing-workflow.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": minor +--- + +Add a built-in Marketing workflow with content-specific columns and prompts for brief, drafting, editorial review, and publishing. diff --git a/docs/workflow-steps.md b/docs/workflow-steps.md index c4e1c3c91d..8819abe208 100644 --- a/docs/workflow-steps.md +++ b/docs/workflow-steps.md @@ -30,6 +30,7 @@ Decision-only or investigation tasks can also declare `noCommitsExpected` / `**N | Coding | `builtin:coding` | Default coding lifecycle and fallback for tasks without an explicit selection. | | Quick fix | `builtin:quick-fix` | Short path for trivial or no-commit/decision work; omits the standard review stage. | | Review-heavy | `builtin:review-heavy` | Standard execute/review/merge path with an additional gated security review. | +| Marketing | `builtin:marketing` | Content pipeline with custom Ideation, Backlog, Drafting, Editorial review, Published, and Archived columns plus marketing brief/draft/editorial prompts; it reuses the standard lifecycle traits and merge-primitive region. | | Compound engineering | `builtin:compound-engineering` | Plugin-gated workflow that invokes Compound Engineering skills for planning, work, review, PR/feedback, and learnings capture. | | 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. | @@ -75,6 +76,8 @@ The default built-in catalog entry `builtin:coding` is backed by the canonical ` `builtin:stepwise-coding` is a separate graph variant backed by `BUILTIN_STEPWISE_CODING_WORKFLOW_IR`; it keeps the same lifecycle columns/traits while modeling per-step parse/execute/review/rework as authored graph structure. +`builtin:marketing` is a non-coding content workflow with marketing-specific columns (`ideation`, `backlog`, `drafting`, `editorial-review`, `published`, `archived`) and prompt seams for content brief, draft, and editorial review. It uses the same lifecycle traits (`intake`, `hold`, `wip`, `merge-blocker`, `human-review`, `complete`, `archived`) and the same merge-gate/branch-group/merge-attempt primitive region as coding workflows, so scheduler, capacity, review blocking, and merge orchestration behavior remain standard. + During triage/planning sessions, agents can call `fn_workflow_list` to discover available built-in and custom workflows and read their descriptions before routing work. They can call `fn_workflow_select` to select a workflow for the task being specified, or pass `workflow_id` when creating child tasks with `fn_task_create`; decision-only or investigation tasks can also set `noCommitsExpected` / `**No commits expected:** true` when no code changes are expected. The built-in triage thresholds, decision-only verb list, and default routing IDs are workflow-native typed settings resolved from the selected workflow. #### Runtime invariant criterion diff --git a/packages/core/src/__tests__/builtin-workflows.test.ts b/packages/core/src/__tests__/builtin-workflows.test.ts index fba1ae2dc6..86241ec8b8 100644 --- a/packages/core/src/__tests__/builtin-workflows.test.ts +++ b/packages/core/src/__tests__/builtin-workflows.test.ts @@ -20,7 +20,12 @@ const EXECUTE_NODE_MAX_RETRIES = 2; describe("built-in workflows", () => { // Non-compiler built-ins model graph-only node kinds or reusable fragments the // linear compiler cannot lower to a step list. They still must parse as valid IR. - const NON_COMPILABLE_BUILTIN_IDS = new Set(["builtin:coding", "builtin:stepwise-coding", "builtin:pr-workflow"]); + const NON_COMPILABLE_BUILTIN_IDS = new Set([ + "builtin:coding", + "builtin:marketing", + "builtin:stepwise-coding", + "builtin:pr-workflow", + ]); it("every built-in has a valid IR; linear built-ins compile without error", () => { expect(BUILTIN_WORKFLOWS.length).toBeGreaterThanOrEqual(4); @@ -150,6 +155,55 @@ describe("built-in workflows", () => { expect(ir.settings).toEqual(BUILTIN_WORKFLOW_SETTINGS); }); + it("includes the marketing built-in with custom columns, prompts, and lifecycle traits", () => { + const marketing = getBuiltinWorkflow("builtin:marketing"); + expect(marketing).toBeDefined(); + expect(marketing!.kind).toBe("workflow"); + expect(BUILTIN_WORKFLOWS.some((workflow) => workflow.id === "builtin:marketing")).toBe(true); + expect(defaultEnabledBuiltinWorkflowIds()).toContain("builtin:marketing"); + expect(() => parseWorkflowIr(marketing!.ir)).not.toThrow(); + + const ir = parseWorkflowIr(marketing!.ir); + expect(ir.version).toBe("v2"); + if (ir.version !== "v2") throw new Error("expected v2"); + + expect(ir.columns.map((column) => column.id)).toEqual([ + "ideation", + "backlog", + "drafting", + "editorial-review", + "published", + "archived", + ]); + + const editorialReview = ir.columns.find((column) => column.id === "editorial-review"); + expect(editorialReview).toBeDefined(); + expect(editorialReview!.traits.map((trait) => trait.trait)).toEqual([ + "merge-blocker", + "human-review", + "stall-detection", + "merge", + ]); + const editorialFlags = resolveColumnFlags(editorialReview!); + expect(editorialFlags.mergeBlocker).toBe(true); + expect(editorialFlags.humanReview).toBe(true); + + const drafting = ir.columns.find((column) => column.id === "drafting"); + expect(drafting).toBeDefined(); + expect(resolveColumnFlags(drafting!).countsTowardWip).toBe(true); + + const execute = ir.nodes.find((node) => node.config?.seam === "execute"); + const review = ir.nodes.find((node) => node.config?.seam === "review"); + expect(execute?.id).toBe("draft"); + expect(execute?.config?.name).toBe("Draft content"); + expect(String(execute?.config?.prompt ?? "")).toContain("marketing copywriter"); + expect(String(execute?.config?.prompt ?? "").length).toBeGreaterThan(100); + expect(review?.id).toBe("editorial"); + expect(review?.config?.name).toBe("Editorial review"); + expect(String(review?.config?.prompt ?? "")).toContain("editorial reviewer"); + expect(String(review?.config?.prompt ?? "").length).toBeGreaterThan(100); + }); + it("includes the design built-in with an ordered design review gate", () => { const design = getBuiltinWorkflow("builtin:design"); expect(design).toBeDefined(); @@ -181,14 +235,16 @@ describe("built-in workflows", () => { ).map((workflow) => workflow.id), ); expect(defaultEnabledBuiltinWorkflowIds()).toContain("builtin:design"); + expect(defaultEnabledBuiltinWorkflowIds()).toContain("builtin:marketing"); expect(defaultEnabledBuiltinWorkflowIds()).not.toContain("builtin:compound-engineering"); expect(defaultEnabledBuiltinWorkflowIds()).not.toContain("builtin:pr-workflow"); expect(getBuiltinWorkflow("builtin:pr-workflow")!.kind).toBe("fragment"); - expect(defaultEnabledBuiltinWorkflowIds().length).toBeGreaterThanOrEqual(4); - expect(defaultEnabledBuiltinWorkflowIds().slice(0, 4)).toEqual([ + expect(defaultEnabledBuiltinWorkflowIds().length).toBeGreaterThanOrEqual(5); + expect(defaultEnabledBuiltinWorkflowIds().slice(0, 5)).toEqual([ "builtin:coding", "builtin:quick-fix", "builtin:review-heavy", + "builtin:marketing", "builtin:stepwise-coding", ]); }); @@ -405,7 +461,7 @@ describe("built-in workflows", () => { }); it("branching built-ins can be selected without throwing", async () => { - for (const workflowId of ["builtin:coding", "builtin:stepwise-coding"]) { + for (const workflowId of ["builtin:coding", "builtin:marketing", "builtin:stepwise-coding"]) { const task = await store.createTask({ description: `select ${workflowId}`, enabledWorkflowSteps: [] }); await expect(store.selectTaskWorkflow(task.id, workflowId)).resolves.toEqual([]); diff --git a/packages/core/src/builtin-marketing-workflow-ir.ts b/packages/core/src/builtin-marketing-workflow-ir.ts new file mode 100644 index 0000000000..ab5039f40b --- /dev/null +++ b/packages/core/src/builtin-marketing-workflow-ir.ts @@ -0,0 +1,116 @@ +import type { WorkflowIr } from "./workflow-ir-types.js"; +import { parseWorkflowIr } from "./workflow-ir.js"; +import { BUILTIN_WORKFLOW_SETTINGS } from "./builtin-workflow-settings.js"; + +/** + * FNXC:WorkflowMarketing 2026-06-20-00:00: + * Fusion needs a non-coding built-in workflow for marketing and content work. Keep the engine pipeline unchanged by reusing the standard lifecycle trait vocabulary and the canonical merge-primitive region while exposing marketing-specific columns and prompts for brief, draft, and editorial review phases. + */ +const RAW_BUILTIN_MARKETING_WORKFLOW_IR: WorkflowIr = { + version: "v2", + name: "builtin-marketing-workflow", + columns: [ + { id: "ideation", name: "Ideation", traits: [{ trait: "intake" }] }, + { + id: "backlog", + name: "Backlog", + traits: [{ trait: "hold", config: { release: "capacity" } }, { trait: "reset-on-entry" }], + }, + { + id: "drafting", + name: "Drafting", + traits: [ + { trait: "wip", config: { limitSetting: "maxConcurrent", countPending: true } }, + { trait: "abort-on-exit" }, + { trait: "timing" }, + ], + }, + { + id: "editorial-review", + name: "Editorial review", + traits: [{ trait: "merge-blocker" }, { trait: "human-review" }, { trait: "stall-detection" }, { trait: "merge" }], + }, + { id: "published", name: "Published", traits: [{ trait: "complete" }] }, + { id: "archived", name: "Archived", traits: [{ trait: "archived" }] }, + ], + nodes: [ + { id: "start", kind: "start", column: "ideation" }, + { + id: "brief", + kind: "prompt", + column: "ideation", + config: { + seam: "planning", + name: "Content brief", + prompt: + "You are a marketing content strategist. Turn this task into a concrete content brief: audience, channel, key message, format, success metric, required source material, and approval constraints.", + }, + }, + { + id: "draft", + kind: "prompt", + column: "drafting", + config: { + seam: "execute", + name: "Draft content", + prompt: + "You are a marketing copywriter executing the approved brief. Produce the requested deliverable, following brand voice, audience intent, channel constraints, format requirements, and the brief's success metric.", + maxRetries: 2, + }, + }, + { + id: "editorial", + kind: "prompt", + column: "editorial-review", + config: { + seam: "review", + name: "Editorial review", + prompt: + "You are an independent editorial reviewer. Check the draft for brand voice, factual accuracy, audience fit, channel fit, CTA clarity, compliance with the brief, and substantive quality issues; block on issues that would harm publication readiness.", + }, + }, + { id: "merge-gate", kind: "merge-gate", column: "editorial-review", config: { gate: "auto-merge" } }, + { id: "merge-retry", kind: "retry-backoff", column: "editorial-review", config: { policy: "merge", maxAttempts: 3 } }, + { id: "merge-manual-hold", kind: "manual-merge-hold", column: "editorial-review", config: { release: "manual" } }, + { + id: "branch-group-member-integration", + kind: "branch-group-member-integration", + column: "editorial-review", + config: { reworkRegion: true, maxReworkCycles: 3 }, + }, + { id: "branch-group-promotion", kind: "branch-group-promotion", column: "editorial-review" }, + { + id: "merge-attempt", + kind: "merge-attempt", + column: "editorial-review", + config: { capability: "task-merge", reworkRegion: true, maxReworkCycles: 3 }, + }, + { id: "recovery-router", kind: "recovery-router", column: "editorial-review", config: { surfaces: ["merge", "retry"] } }, + { id: "end", kind: "end", column: "published" }, + ], + edges: [ + { from: "start", to: "brief" }, + { from: "brief", to: "draft", condition: "success" }, + { from: "draft", to: "editorial", condition: "success" }, + { from: "editorial", to: "merge-gate", condition: "success" }, + { from: "merge-gate", to: "branch-group-member-integration", condition: "outcome:auto-on" }, + { from: "merge-gate", to: "merge-manual-hold", condition: "outcome:auto-off" }, + { from: "merge-retry", to: "merge-attempt", condition: "success", kind: "rework" }, + { from: "merge-manual-hold", to: "branch-group-member-integration", condition: "success", kind: "rework" }, + { from: "branch-group-member-integration", to: "branch-group-promotion", condition: "success" }, + { from: "branch-group-member-integration", to: "merge-manual-hold", condition: "outcome:manual-required" }, + { from: "branch-group-promotion", to: "merge-attempt", condition: "success" }, + { from: "branch-group-promotion", to: "merge-manual-hold", condition: "outcome:manual-required" }, + { from: "merge-attempt", to: "end", condition: "success" }, + { from: "merge-attempt", to: "merge-retry", condition: "outcome:transient-failure" }, + { from: "merge-attempt", to: "merge-manual-hold", condition: "outcome:manual-required" }, + { from: "recovery-router", to: "merge-attempt", condition: "outcome:wake-merge", kind: "rework" }, + { from: "brief", to: "end", condition: "failure" }, + { from: "draft", to: "end", condition: "failure" }, + { from: "editorial", to: "end", condition: "failure" }, + { from: "merge-attempt", to: "end", condition: "failure" }, + ], + settings: BUILTIN_WORKFLOW_SETTINGS, +}; + +export const BUILTIN_MARKETING_WORKFLOW_IR = parseWorkflowIr(RAW_BUILTIN_MARKETING_WORKFLOW_IR); diff --git a/packages/core/src/builtin-workflows.ts b/packages/core/src/builtin-workflows.ts index 7e443a698f..6d7aed26e1 100644 --- a/packages/core/src/builtin-workflows.ts +++ b/packages/core/src/builtin-workflows.ts @@ -1,4 +1,5 @@ import { BUILTIN_CODING_WORKFLOW_IR } from "./builtin-coding-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"; import { BUILTIN_WORKFLOW_SETTINGS } from "./builtin-workflow-settings.js"; @@ -150,6 +151,29 @@ export const BUILTIN_WORKFLOWS: WorkflowDefinition[] = [ { id: "merge", kind: "prompt", config: builtinPromptConfig("merge", "Merge boundary") }, ], }), + { + id: "builtin:marketing", + name: "Marketing (built-in)", + description: "Marketing content pipeline: ideate, brief, draft, editorial review, then publish via the standard lifecycle merge primitives.", + kind: "workflow", + ir: BUILTIN_MARKETING_WORKFLOW_IR, + layout: { + start: { x: 60, y: 160 }, + brief: { x: 230, y: 160 }, + draft: { x: 400, y: 160 }, + editorial: { x: 570, y: 160 }, + "merge-gate": { x: 740, y: 160 }, + "branch-group-member-integration": { x: 910, y: 80 }, + "branch-group-promotion": { x: 1080, y: 80 }, + "merge-attempt": { x: 1250, y: 160 }, + "merge-retry": { x: 1420, y: 80 }, + "recovery-router": { x: 1420, y: 240 }, + "merge-manual-hold": { x: 910, y: 240 }, + end: { x: 1590, y: 160 }, + }, + createdAt: BUILTIN_TS, + updatedAt: BUILTIN_TS, + }, linear({ id: "builtin:compound-engineering", name: "Compound engineering (built-in)", diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index be90303393..1b24415de7 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -117,6 +117,7 @@ export type { EffectiveAgentResult, } from "./column-agent-resolver.js"; export { BUILTIN_CODING_WORKFLOW_IR } from "./builtin-coding-workflow-ir.js"; +export { BUILTIN_MARKETING_WORKFLOW_IR } from "./builtin-marketing-workflow-ir.js"; 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";