Skip to content

Commit 6f83872

Browse files
authored
unify step code for app and export (#96)
* better codegen * better creds * linear and firecrawl * ai gateway * v0 * slack * update docs * fix build * fix docs * fix missing getErrorMessage
1 parent 2f84f3b commit 6f83872

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1579
-713
lines changed

CONTRIBUTING.md

Lines changed: 80 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -196,13 +196,12 @@ The plugin system uses a **modular file structure** where each integration is se
196196

197197
```
198198
plugins/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
222222
mkdir -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
295310
import "server-only";
296311

297312
import { fetchCredentials } from "@/lib/credential-fetcher";
298313
import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
299314
import { getErrorMessage } from "@/lib/utils";
315+
import type { MyIntegrationCredentials } from "../credentials";
300316

301317
type 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
*/
366385
export 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
438414
import type { IntegrationPlugin } from "../registry";
439415
import { registerIntegration } from "../registry";
440-
import { sendMessageCodegenTemplate } from "./codegen/send-message";
441416
import { MyIntegrationIcon } from "./icon";
442417

443418
const 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
542514
pnpm 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
662632
import "server-only";
663633

664634
import { fetchCredentials } from "@/lib/credential-fetcher";
665635
import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
666636
import { getErrorMessage } from "@/lib/utils";
637+
import type { MyIntegrationCredentials } from "../credentials";
667638

668639
type 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
702677
export 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

777760
1. 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
784767
3. Test thoroughly
785768

786769
Each integration is self-contained in one organized directory, making it easy to develop, test, and maintain. Happy building!

0 commit comments

Comments
 (0)