Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions packages/nodes/src/ai/__tests__/multi-channel-campaign.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, it, expect } from "vitest";
import { multiChannelCampaignNode } from "../multi-channel-campaign.js";
import type { NodeExecutionContext } from "@jam-nodes/core";

const dummyContext: NodeExecutionContext = {
userId: "test_user",
workflowExecutionId: "wf_test",
variables: {},
resolveNestedPath: () => undefined,
};

describe("multiChannelCampaignNode", () => {
it("generates campaign copy with correct placeholders", async () => {
const input = {
productDescription: "SpreadJam SuperTool",
targetAudience: "developers",
tone: "quirky" as const,
};

const result = await multiChannelCampaignNode.executor(input, dummyContext);
expect(result.success).toBe(true);
const out = result.output!;
expect(out.emailSubject).toContain("Quirky");
expect(out.emailSubject).toContain("SpreadJam SuperTool");
expect(out.emailSubject).toContain("developers");
expect(out.emailBody).toContain("Hey developers");
expect(out.tweet).toContain("SpreadJam SuperTool");
expect(out.tweet).toContain("developers");
expect(out.tweet).toContain("quirky");
expect(out.linkedInPost).toContain("SpreadJam SuperTool");
expect(out.linkedInPost).toContain("developers");
expect(out.linkedInPost).toContain("quirky");
});
});
14 changes: 11 additions & 3 deletions packages/nodes/src/ai/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export {
SocialKeywordGeneratorOutputSchema,
type SocialKeywordGeneratorInput,
type SocialKeywordGeneratorOutput,
} from './keyword-generator.js';
} from "./keyword-generator.js";

export {
draftEmailsNode,
Expand All @@ -16,7 +16,7 @@ export {
type DraftEmailsOutput,
type DraftEmailInfo,
type Contact,
} from './draft-emails.js';
} from "./draft-emails.js";

export {
socialAiAnalyzeNode,
Expand All @@ -26,4 +26,12 @@ export {
type SocialAiAnalyzeOutput,
type SocialPost,
type AnalyzedPost,
} from './analyze-posts.js';
} from "./analyze-posts.js";

export {
multiChannelCampaignNode,
MultiChannelCampaignInputSchema,
MultiChannelCampaignOutputSchema,
type MultiChannelCampaignInput,
type MultiChannelCampaignOutput,
} from "./multi-channel-campaign.js";
100 changes: 100 additions & 0 deletions packages/nodes/src/ai/multi-channel-campaign.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Multi-Channel Campaign Node
// Generates campaign copy for email, tweet, and LinkedIn post based on a product description,
// target audience, and tone. Pure TypeScript, no external API calls.

import { defineNode } from "@jam-nodes/core";
import { z } from "zod";

// ---------------------------------------------------------------------------
// Schemas
// ---------------------------------------------------------------------------

export const MultiChannelCampaignInputSchema = z.object({
/** Brief description of the product or service */
productDescription: z.string().min(1),
/** Who the campaign is aimed at — e.g., "developers", "marketers" */
targetAudience: z.string().min(1),
/** Desired writing tone */
tone: z.enum(["friendly", "professional", "quirky"] as const),
});

export const MultiChannelCampaignOutputSchema = z.object({
/** Subject line for the email */
emailSubject: z.string(),
/** Full email body */
emailBody: z.string(),
/** A tweet-sized message (max 280 chars) */
tweet: z.string(),
/** LinkedIn post copy */
linkedInPost: z.string(),
});

export type MultiChannelCampaignInput = z.infer<
typeof MultiChannelCampaignInputSchema
>;
export type MultiChannelCampaignOutput = z.infer<
typeof MultiChannelCampaignOutputSchema
>;

// ---------------------------------------------------------------------------
// Helpers – simple template fillers
// ---------------------------------------------------------------------------

function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}

function generateEmailSubject(input: MultiChannelCampaignInput): string {
const { productDescription, targetAudience, tone } = input;
return `${capitalize(tone)} ${productDescription} for ${targetAudience}`;
}

function generateEmailBody(input: MultiChannelCampaignInput): string {
const { productDescription, targetAudience, tone } = input;
return `Hey ${targetAudience},\n\nWe’re thrilled to launch ${productDescription}. It’s designed to be ${tone} and will help you achieve great results.\n\nCheers,\nYour Team`;
}

function generateTweet(input: MultiChannelCampaignInput): string {
const { productDescription, targetAudience, tone } = input;
const tag = productDescription.replace(/\s+/g, "");
return `🚀 ${productDescription} is now ${tone} for ${targetAudience}! #${tag}`;
Comment thread
manas-tr marked this conversation as resolved.
}

function generateLinkedInPost(input: MultiChannelCampaignInput): string {
const { productDescription, targetAudience, tone } = input;
return `Introducing ${productDescription} — a ${tone} solution built for ${targetAudience}. We’ve crafted it to empower you and drive success. Learn more at https://example.com`;
}

// ---------------------------------------------------------------------------
// Node definition
// ---------------------------------------------------------------------------

export const multiChannelCampaignNode = defineNode({
type: "multi_channel_campaign",
name: "Multi‑Channel Campaign",
description: "Generate email, tweet, and LinkedIn copy for a product launch",
category: "action",
inputSchema: MultiChannelCampaignInputSchema,
outputSchema: MultiChannelCampaignOutputSchema,
estimatedDuration: 5,
capabilities: {
supportsRerun: true,
supportsBulkActions: true,
},
executor: async (input, _context) => {
const emailSubject = generateEmailSubject(input);
const emailBody = generateEmailBody(input);
const tweet = generateTweet(input);
const linkedInPost = generateLinkedInPost(input);

return {
success: true,
output: {
emailSubject,
emailBody,
tweet,
linkedInPost,
},
};
},
});
40 changes: 21 additions & 19 deletions packages/nodes/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export {
DelayOutputSchema,
WebhookTriggerInputSchema,
WebhookTriggerOutputSchema,
} from './logic/index.js'
} from "./logic/index.js";

export type {
ConditionalInput,
Expand All @@ -27,7 +27,7 @@ export type {
DelayOutput,
WebhookTriggerInput,
WebhookTriggerOutput,
} from './logic/index.js'
} from "./logic/index.js";

// Transform nodes
export {
Expand All @@ -42,7 +42,7 @@ export {
SortInputSchema,
SortOutputSchema,
SortDirectionSchema,
} from './transform/index.js'
} from "./transform/index.js";

export type {
MapInput,
Expand All @@ -53,7 +53,7 @@ export type {
SortInput,
SortOutput,
SortDirection,
} from './transform/index.js'
} from "./transform/index.js";

// Example nodes
export {
Expand All @@ -64,15 +64,15 @@ export {
breadNode,
BreadInputSchema,
BreadOutputSchema,
} from './examples/index.js'
} from "./examples/index.js";

export type {
HttpRequestInput,
HttpRequestOutput,
HttpMethod,
BreadInput,
BreadOutput,
} from './examples/index.js'
} from "./examples/index.js";

// Integration nodes
export {
Expand Down Expand Up @@ -196,7 +196,7 @@ export {
readOutputSchema,
updateInputSchema,
updateOutputSchema,
} from './integrations/index.js'
} from "./integrations/index.js";

export type {
RedditMonitorInput,
Expand Down Expand Up @@ -276,7 +276,7 @@ export type {
ReadOutput,
UpdateInput,
UpdateOutput,
} from './integrations/index.js'
} from "./integrations/index.js";

// AI nodes
export {
Expand All @@ -291,7 +291,7 @@ export {
socialAiAnalyzeNode,
SocialAiAnalyzeInputSchema,
SocialAiAnalyzeOutputSchema,
} from './ai/index.js'
} from "./ai/index.js";

export type {
SocialKeywordGeneratorInput,
Expand All @@ -304,15 +304,15 @@ export type {
SocialAiAnalyzeOutput,
SocialPost,
AnalyzedPost,
} from './ai/index.js'
} from "./ai/index.js";

// All nodes as a collection
import { conditionalNode } from './logic/index.js'
import { endNode } from './logic/index.js'
import { delayNode } from './logic/index.js'
import { webhookTriggerNode } from './logic/index.js'
import { mapNode, filterNode, sortNode } from './transform/index.js'
import { httpRequestNode, breadNode } from './examples/index.js'
import { conditionalNode } from "./logic/index.js";
import { endNode } from "./logic/index.js";
import { delayNode } from "./logic/index.js";
import { webhookTriggerNode } from "./logic/index.js";
import { mapNode, filterNode, sortNode } from "./transform/index.js";
import { httpRequestNode, breadNode } from "./examples/index.js";
import {
redditMonitorNode,
twitterMonitorNode,
Expand Down Expand Up @@ -348,12 +348,13 @@ import {
googleSheetsClearNode,
googleSheetsReadNode,
googleSheetsUpdateNode,
} from './integrations/index.js'
} from "./integrations/index.js";
import {
socialKeywordGeneratorNode,
draftEmailsNode,
socialAiAnalyzeNode,
} from './ai/index.js'
multiChannelCampaignNode,
} from "./ai/index.js";

/**
* All built-in nodes as an array for easy registration
Expand Down Expand Up @@ -411,4 +412,5 @@ export const builtInNodes = [
socialKeywordGeneratorNode,
draftEmailsNode,
socialAiAnalyzeNode,
]
multiChannelCampaignNode,
Comment thread
manas-tr marked this conversation as resolved.
];