Getting Started
Build your first custom integration in 5 minutes
This guide walks you through creating a minimal "Hello World" integration that logs a message and passes data through. By the end, you'll have a .bundle.tar.gz file ready to upload.
Prerequisites
- Bun v1.1+ (or Node.js 20+)
- TypeScript 5.7+
Project Setup
Fastest path: scaffold the whole project with the CLI instead of the manual steps below —
bunx @scrydon/sdk-authoring integrations init my-vendor --outDir ./integrations
cd integrations/my-vendorThe interactive prompts pick the auth type (OAuth, API key, or none), the capabilities to scaffold, and a brand color, then tailor the generated src/index.ts to your selections. Pass --yes to skip prompts and accept defaults (auth = none, capabilities = blocks & tools) — useful for CI. Either way you get package.json, tsconfig.json, a defineVendor() skeleton, and a sample live test under src/__tests__/.
mkdir my-integration && cd my-integrationbun init -yUpdate package.json:
{
"name": "my-integration",
"version": "1.0.0",
"private": true,
"type": "module",
"main": "src/index.ts"
}bun add -d @scrydon/sdk-authoring zodnpm install --save-dev @scrydon/sdk-authoring zodWrite the Integration
Create src/index.ts:
import {
defineVendor,
defineProduct,
defineTool,
defineBlock,
} from "@scrydon/sdk-authoring/integrations/define";
import type { ToolResponse } from "@scrydon/sdk-authoring/integrations/context";
import { z } from "zod";
// 1. Define a tool — the runtime logic
const sayHelloTool = defineTool({
id: "say-hello",
name: "Say Hello",
version: "1.0.0",
description: "Logs a greeting and passes the message through.",
input: z.object({
message: z.string().describe("The message to pass through"),
}),
output: z.object({
message: z.string().describe("The original message, unchanged"),
}),
params: {
message: {
type: "string",
required: true,
visibility: "user-or-llm",
description: "Message to pass through the block",
},
},
async execute(input, ctx): Promise<ToolResponse<{ message: string }>> {
ctx.logger.info(`Hello World! Received: "${input.message}"`);
return {
success: true,
output: { message: input.message },
};
},
});
// 2. Define a block — the workflow editor UI
const helloBlock = defineBlock({
type: "hello_world",
name: "Hello World",
description: "Logs a greeting and passes input to output.",
category: "integration",
bgColor: "#22C55E",
authMode: "none",
subBlocks: [
{
id: "message",
title: "Message",
type: "short-input",
placeholder: "Enter a message…",
required: true,
},
],
tools: {
access: ["say-hello"],
},
inputs: {
message: { type: "string", description: "Incoming message" },
},
outputs: {
message: { type: "string", description: "Outgoing message" },
},
});
// 3. Define a product — the grouping unit
const helloProduct = defineProduct({
id: "hello-world",
name: "Hello World",
description: "A minimal demo integration.",
icon: "./assets/icon.svg",
capabilities: {
tools: [sayHelloTool],
triggers: [],
},
block: helloBlock,
});
// 4. Export the vendor — the top-level container
export default defineVendor({
id: "hello-world",
name: "Hello World",
version: "1.0.0",
description: "A minimal example integration.",
color: "#22C55E",
icon: "./assets/icon.svg",
categories: ["integration"],
connectivity: "local",
auth: {
credentials: { none: { type: "none" } },
default: "none",
},
products: [helloProduct],
});The entry point must have a default export that is a defineVendor() result. The build CLI reads this to extract the manifest.
Tool ID shorthand — You can use short tool IDs like "say-hello" instead of the fully qualified "hello-world:hello-world:say-hello". The SDK auto-qualifies short IDs by prepending {vendorId}:{productId}: during defineVendor(). All first-party integrations use the short form.
Understanding the Layers
The code above has four layers, each with a specific role:
| Layer | What it defines | Key fields |
|---|---|---|
defineTool() | Runtime logic | input (Zod), output (Zod), execute() function |
defineBlock() | Workflow editor UI | subBlocks (form fields), inputs/outputs (data flow) |
defineProduct() | Grouping unit | capabilities.tools, block, gets enabled/disabled per org |
defineVendor() | Top-level container | auth config, products array, vendor metadata |
Data flow: The block's subBlocks define the form fields. The block's tools.access array links to tool IDs. When the workflow runs, the platform calls the tool's execute() function with the validated input and a PureContext.
Build the Bundle
bunx @scrydon/sdk-authoring integrations buildOutput:
Bundle created: dist/hello-world-1.0.0.bundle.tar.gz
Vendor: Hello World (hello-world)
Version: 1.0.0
Products: 1
Tools: 1
Dependencies: 1 (SBOM: meta/sbom.cdx.json)What the Build Does
- Imports your entry point and verifies the default export is a
defineVendor()result - Extracts all metadata into
manifest.json— Zod schemas become JSON Schemas - Bundles your code with esbuild into a single
dist/index.js(all dependencies inlined) - Generates a CycloneDX 1.6 SBOM listing all bundled NPM packages (
meta/sbom.cdx.json) - Packages everything into
{vendorId}-{version}.bundle.tar.gz
Inspect the Output
# List archive contents
tar -tzf dist/hello-world-1.0.0.bundle.tar.gz
# Pretty-print the manifest
tar -xzf dist/hello-world-1.0.0.bundle.tar.gz -O manifest.json | python3 -m json.toolValidate Without Uploading
The SDK includes a test command:
# Static validation — checks manifest, schemas, ID formats
bunx @scrydon/sdk-authoring integrations test --level static
# Sandbox validation — loads the bundle in a Worker Thread
bunx @scrydon/sdk-authoring integrations test --level sandbox
# Test a specific tool
bunx @scrydon/sdk-authoring integrations test --level sandbox --tool hello-world:hello-world:say-helloUpload and Use
See the Uploading guide for detailed instructions. The quick version:
- Go to Settings > Platform > Integrations and select the Custom tab
- Choose Manual upload, then drag your
.bundle.tar.gzonto the upload area - Click Upload
- Open the Workflow Editor and search for "Hello World"
Next Steps: Add Authentication
Most real integrations need credentials. Here's how to add API key authentication:
// Change the auth config
export default defineVendor({
// ...
auth: {
credentials: {
apiKey: {
type: "apiKey",
label: "API Key",
description: "Your service API key",
headerName: "Authorization",
headerPrefix: "Bearer",
},
},
default: "apiKey",
},
// ...
});Update the block to show an API key input:
const myBlock = defineBlock({
// ...
authMode: "apiKey",
subBlocks: [
{
id: "apiKey",
title: "API Key",
type: "short-input",
password: true,
placeholder: "Enter your API key",
},
// ... other fields
],
// ...
});The key is then available in ctx.auth.apiKey inside your execute() function:
async execute(input, ctx) {
const response = await fetch("https://api.example.com/data", {
headers: {
Authorization: `Bearer ${ctx.auth.apiKey}`,
},
});
const data = await response.json();
return { success: true, output: data };
},Next Steps: Multiple Tools
Add a second tool to the same product by defining another defineTool() and including it in capabilities.tools:
const reverseTool = defineTool({
id: "reverse",
name: "Reverse Message",
version: "1.0.0",
description: "Reverses the input message.",
input: z.object({ message: z.string() }),
output: z.object({ message: z.string() }),
params: {
message: {
type: "string",
required: true,
visibility: "user-or-llm",
description: "Message to reverse",
},
},
async execute(input, ctx) {
const reversed = input.message.split("").reverse().join("");
ctx.logger.info(`Reversed: "${reversed}"`);
return { success: true, output: { message: reversed } };
},
});
// Add to capabilities and block
const helloProduct = defineProduct({
// ...
capabilities: {
tools: [sayHelloTool, reverseTool],
triggers: [],
},
block: defineBlock({
// ...
subBlocks: [
{
id: "operation",
title: "Operation",
type: "dropdown",
options: [
{ label: "Say Hello", id: "say-hello" },
{ label: "Reverse", id: "reverse" },
],
},
// ... message field
],
tools: {
access: ["say-hello", "reverse"],
config: {
tool: (params) =>
params.operation === "reverse"
? "reverse"
: "say-hello",
},
},
}),
});The tools.config.tool function dynamically selects which tool to run based on the user's dropdown selection.