@@ -196,13 +196,12 @@ The plugin system uses a **modular file structure** where each integration is se
196196
197197```
198198plugins/my-integration/
199+ ├── credentials.ts # Credential type definition
199200├── icon.tsx # Icon component (SVG)
200- ├── test .ts # Connection test function
201+ ├── index .ts # Plugin definition (ties everything together)
201202├── steps/ # Action implementations
202- │ └── my-action.ts # Server-side step function
203- ├── codegen/ # Export templates for standalone workflows
204- │ └── my-action.ts # Code generation template
205- └── index.ts # Plugin definition (ties everything together)
203+ │ └── my-action.ts # Server-side step function with stepHandler
204+ └── test.ts # Connection test function
206205```
207206
208207** Key Benefits:**
@@ -213,14 +212,14 @@ plugins/my-integration/
213212- ** Self-contained** : No scattered files across the codebase
214213- ** Auto-discovered** : Automatically detected by ` pnpm discover-plugins `
215214- ** Declarative** : Action config fields defined as data, not React components
215+ - ** Write once** : Step logic works for both the app and exported workflows
216216
217217### Step-by-Step Plugin Creation
218218
219219#### Step 1: Create Plugin Directory Structure
220220
221221``` bash
222222mkdir -p plugins/my-integration/steps
223- mkdir -p plugins/my-integration/codegen
224223```
225224
226225#### Step 2: Create Icon Component
@@ -246,7 +245,20 @@ export function MyIntegrationIcon({ className }: { className?: string }) {
246245
247246** OR** use a Lucide icon directly in your index.ts (skip this file if using Lucide).
248247
249- #### Step 3: Create Test Function
248+ #### Step 3: Create Credentials Type
249+
250+ ** File:** ` plugins/my-integration/credentials.ts `
251+
252+ This defines the credential type shared between app and export code:
253+
254+ ``` typescript
255+ export type MyIntegrationCredentials = {
256+ MY_INTEGRATION_API_KEY? : string ;
257+ // Add other credential fields as needed
258+ };
259+ ```
260+
261+ #### Step 4: Create Test Function
250262
251263** File:** ` plugins/my-integration/test.ts `
252264
@@ -285,38 +297,46 @@ export async function testMyIntegration(credentials: Record<string, string>) {
285297}
286298```
287299
288- #### Step 4 : Create Step Function (Server Logic)
300+ #### Step 5 : Create Step Function (Server Logic)
289301
290302** File:** ` plugins/my-integration/steps/send-message.ts `
291303
292- This runs on the server during workflow execution. Steps use the ` withStepLogging ` wrapper to automatically log execution for the workflow builder UI:
304+ This runs on the server during workflow execution. Steps have two parts:
305+
306+ 1 . ` stepHandler ` - Core logic that receives credentials as a parameter
307+ 2 . ` sendMessageStep ` - Entry point that fetches credentials and wraps with logging
293308
294309``` typescript
295310import " server-only" ;
296311
297312import { fetchCredentials } from " @/lib/credential-fetcher" ;
298313import { type StepInput , withStepLogging } from " @/lib/steps/step-handler" ;
299314import { getErrorMessage } from " @/lib/utils" ;
315+ import type { MyIntegrationCredentials } from " ../credentials" ;
300316
301317type SendMessageResult =
302318 | { success: true ; id: string ; url: string }
303319 | { success: false ; error: string };
304320
305- // Extend StepInput to get automatic logging context
306- export type SendMessageInput = StepInput & {
307- integrationId? : string ;
321+ // Core input fields (without app-specific context)
322+ export type SendMessageCoreInput = {
308323 message: string ;
309324 channel: string ;
310325};
311326
327+ // App input includes integrationId and step context
328+ export type SendMessageInput = StepInput &
329+ SendMessageCoreInput & {
330+ integrationId? : string ;
331+ };
332+
312333/**
313- * Send message logic - separated for clarity and testability
334+ * Core logic
314335 */
315- async function sendMessage(input : SendMessageInput ): Promise <SendMessageResult > {
316- const credentials = input .integrationId
317- ? await fetchCredentials (input .integrationId )
318- : {};
319-
336+ async function stepHandler(
337+ input : SendMessageCoreInput ,
338+ credentials : MyIntegrationCredentials
339+ ): Promise <SendMessageResult > {
320340 const apiKey = credentials .MY_INTEGRATION_API_KEY ;
321341
322342 if (! apiKey ) {
@@ -360,73 +380,29 @@ async function sendMessage(input: SendMessageInput): Promise<SendMessageResult>
360380}
361381
362382/**
363- * Send Message Step
364- * Sends a message using My Integration API
383+ * App entry point - fetches credentials and wraps with logging
365384 */
366385export async function sendMessageStep(
367386 input : SendMessageInput
368387): Promise <SendMessageResult > {
369388 " use step" ;
370- return withStepLogging (input , () => sendMessage (input ));
371- }
372- ```
373-
374- ** Key Points:**
375-
376- 1 . ** Extend ` StepInput ` ** : Your input type should extend ` StepInput ` to include the optional ` _context ` for logging
377- 2 . ** Separate logic function** : Keep the actual logic in a separate function for clarity and testability
378- 3 . ** Wrap with ` withStepLogging ` ** : The step function just wraps the logic with ` withStepLogging(input, () => logic(input)) `
379- 4 . ** Return success/error objects** : Steps should return ` { success: true, ... } ` or ` { success: false, error: "..." } `
380-
381- #### Step 5: Create Codegen Template
382389
383- ** File:** ` plugins/my-integration/codegen/send-message.ts `
384-
385- This template is used when users export/download their workflow as standalone code:
386-
387- ``` typescript
388- /**
389- * Code generation template for Send Message action
390- * Used when exporting workflows to standalone Next.js projects
391- */
392- export const sendMessageCodegenTemplate = `
393- export async function sendMessageStep(input: {
394- message: string;
395- channel: string;
396- }) {
397- "use step";
398-
399- const apiKey = process.env.MY_INTEGRATION_API_KEY;
390+ const credentials = input .integrationId
391+ ? await fetchCredentials (input .integrationId )
392+ : {};
400393
401- if (!apiKey) {
402- throw new Error('MY_INTEGRATION_API_KEY environment variable is required');
403- }
394+ return withStepLogging (input , () => stepHandler (input , credentials ));
395+ }
404396
405- const response = await fetch('https://api.my-integration.com/messages', {
406- method: 'POST',
407- headers: {
408- 'Content-Type': 'application/json',
409- 'Authorization': \` Bearer \$ {apiKey}\` ,
410- },
411- body: JSON.stringify({
412- message: input.message,
413- channel: input.channel,
414- }),
415- });
416-
417- if (!response.ok) {
418- throw new Error(\` API request failed: \$ {response.statusText}\` );
419- }
397+ export const _integrationType = " my-integration" ;
398+ ```
420399
421- const result = await response.json();
400+ ** Key Points: **
422401
423- return {
424- id: result.id,
425- url: result.url,
426- success: true,
427- };
428- } ` ;
429- ```
402+ 1 . ** ` stepHandler ` ** : Contains the core business logic, receives credentials as a parameter
403+ 2 . ** ` [action]Step ` ** : Entry point that fetches credentials and wraps with logging
404+ 3 . ** ` _integrationType ` ** : Integration identifier for this step
405+ 4 . ** Credentials type** : Import from ` ../credentials ` for type safety
430406
431407#### Step 6: Create Plugin Index
432408
@@ -437,7 +413,6 @@ This ties everything together. The plugin uses a **declarative approach** where
437413``` typescript
438414import type { IntegrationPlugin } from " ../registry" ;
439415import { registerIntegration } from " ../registry" ;
440- import { sendMessageCodegenTemplate } from " ./codegen/send-message" ;
441416import { MyIntegrationIcon } from " ./icon" ;
442417
443418const myIntegrationPlugin: IntegrationPlugin = {
@@ -478,6 +453,7 @@ const myIntegrationPlugin: IntegrationPlugin = {
478453 " my-integration-sdk" : " ^1.0.0" ,
479454 },
480455
456+ // Actions provided by this integration
481457 actions: [
482458 {
483459 slug: " send-message" , // Action ID: "my-integration/send-message"
@@ -502,7 +478,6 @@ const myIntegrationPlugin: IntegrationPlugin = {
502478 placeholder: " #general" ,
503479 },
504480 ],
505- codegenTemplate: sendMessageCodegenTemplate ,
506481 },
507482 // Add more actions as needed
508483 ],
@@ -533,10 +508,7 @@ export default myIntegrationPlugin;
533508
534509#### Step 7: Run Plugin Discovery
535510
536- The ` discover-plugins ` script auto-generates:
537- - ` plugins/index.ts ` - Import registry
538- - ` lib/types/integration.ts ` - IntegrationType union
539- - ` lib/step-registry.ts ` - Step function mappings
511+ The ` discover-plugins ` script auto-generates type definitions and registries:
540512
541513``` bash
542514pnpm discover-plugins
@@ -632,7 +604,6 @@ actions: [
632604 { key: " message" , label: " Message" , type: " template-input" },
633605 { key: " channel" , label: " Channel" , type: " text" },
634606 ],
635- codegenTemplate: sendMessageCodegenTemplate ,
636607 },
637608 {
638609 slug: " create-record" ,
@@ -645,7 +616,6 @@ actions: [
645616 { key: " title" , label: " Title" , type: " template-input" , required: true },
646617 { key: " description" , label: " Description" , type: " template-textarea" },
647618 ],
648- codegenTemplate: createRecordCodegenTemplate ,
649619 },
650620],
651621```
@@ -656,28 +626,33 @@ actions: [
656626
657627### Pattern 1: Step Function Structure
658628
659- Steps follow a consistent structure with logging :
629+ Steps follow a consistent structure:
660630
661631``` typescript
662632import " server-only" ;
663633
664634import { fetchCredentials } from " @/lib/credential-fetcher" ;
665635import { type StepInput , withStepLogging } from " @/lib/steps/step-handler" ;
666636import { getErrorMessage } from " @/lib/utils" ;
637+ import type { MyIntegrationCredentials } from " ../credentials" ;
667638
668639type MyResult = { success: true ; data: string } | { success: false ; error: string };
669640
670- export type MyInput = StepInput & {
671- integrationId ? : string ;
641+ // Core input (without app-specific fields)
642+ export type MyCoreInput = {
672643 field1: string ;
673644};
674645
675- // 1. Logic function (no "use step" needed)
676- async function myLogic(input : MyInput ): Promise <MyResult > {
677- const credentials = input .integrationId
678- ? await fetchCredentials (input .integrationId )
679- : {};
646+ // App input (extends core with integrationId and step context)
647+ export type MyInput = StepInput & MyCoreInput & {
648+ integrationId? : string ;
649+ };
680650
651+ // 1. stepHandler - Core logic, receives credentials as parameter
652+ async function stepHandler(
653+ input : MyCoreInput ,
654+ credentials : MyIntegrationCredentials
655+ ): Promise <MyResult > {
681656 const apiKey = credentials .MY_INTEGRATION_API_KEY ;
682657 if (! apiKey ) {
683658 return { success: false , error: " API key not configured" };
@@ -698,11 +673,19 @@ async function myLogic(input: MyInput): Promise<MyResult> {
698673 }
699674}
700675
701- // 2. Step wrapper (has "use step", wraps with logging)
676+ // 2. App entry point - fetches credentials and wraps with logging
702677export async function myStep(input : MyInput ): Promise <MyResult > {
703678 " use step" ;
704- return withStepLogging (input , () => myLogic (input ));
679+
680+ const credentials = input .integrationId
681+ ? await fetchCredentials (input .integrationId )
682+ : {};
683+
684+ return withStepLogging (input , () => stepHandler (input , credentials ));
705685}
686+
687+ // 3. Integration identifier
688+ export const _integrationType = " my-integration" ;
706689```
707690
708691### Pattern 2: Declarative Config Fields
@@ -775,12 +758,12 @@ If you run into issues:
775758** Adding an integration requires:**
776759
7777601 . Create plugin directory with 4-5 files:
778- - ` index .ts` - Plugin definition
761+ - ` credentials .ts` - Credential type definition
779762 - ` icon.tsx ` - Icon component (or use Lucide)
763+ - ` index.ts ` - Plugin definition
764+ - ` steps/[action].ts ` - Step function(s) with ` stepHandler `
780765 - ` test.ts ` - Connection test function
781- - ` steps/[action].ts ` - Step function(s)
782- - ` codegen/[action].ts ` - Code generation template(s)
783- 2 . Run ` pnpm discover-plugins ` to auto-generate types
766+ 2 . Run ` pnpm discover-plugins ` to register the plugin
7847673 . Test thoroughly
785768
786769Each integration is self-contained in one organized directory, making it easy to develop, test, and maintain. Happy building!
0 commit comments