Connector examples

Follow end-to-end JSON, builder, local execution, decorator, and catalog connector samples.

Technical review: The decorator sample references PaystackImplementation, which is not defined in the supplied source. Treat that identifier as unresolved until the SDK example is corrected.

Sample 1: JSON → Push → Use in Workflow#

const { FlosyncClient, flosync, variable: v } = require('cliodot-flosync');

const connectorDef = require('./connectors/my-api.json');
const client = new FlosyncClient({ baseUrl, apiKey, apiSecret });
await client.authenticate();
await client.connectors.push(connectorDef);
await client.connectors.install('my-api', { auth: { token: process.env.MY_TOKEN } });

const w = flosync.workflow('use-my-api')
 .http('POST', '/process')
 .step('call', s => s.connector('my-api', 'Create Item', { body: v.body() }))
 .step('respond', s => s.responder('json', { body: v.stepResult('call') }))
 .build();
flosync.register(w);

Sample 2: ConnectorBuilder → Push → Use in Workflow#

const { FlosyncClient, connector, flosync, variable: v } = require('cliodot-flosync');

const client = new FlosyncClient({ baseUrl, apiKey, apiSecret });
await client.authenticate();

const myConnector = connector('weather-api', 'Weather API')
 .baseUrl('https://api.weather.com/v1')
 .apiKey('')
 .get('Get Current', '/current', { params: { city: '{{ vars.city }}', units: 'metric' } })
 .meta({ category: 'Weather', logo: '...', status: 'active', visibility_settings: { visibility: 'private', public_access: 'none' } })
 .build();

await client.connectors.push(myConnector);
await client.connectors.install('weather-api', { auth: { api_key: process.env.WEATHER_API_KEY } });

const w = flosync.workflow('weather')
 .http('GET', '/weather')
 .step('fetch', s => s.connector('weather-api', 'Get Current', { vars: { city: v.query('city') } }))
 .step('respond', s => s.responder('json', { body: v.stepResult('fetch') }))
 .build();
flosync.register(w);

Sample 3: Custom Execute → Register → Use in Workflow#

const { flosync, variable: v } = require('cliodot-flosync');

const slugifyConnector = {
 _id: 'util.slugify',
 type: 'system',
 name: 'Slugify',
 meta: { category: 'utility' },
 auth: { type: 'none' },
 execute: async (action, options) => {
   const text = options.body?.text ?? options.text ?? '';
   return { value: text.toLowerCase().replace(/\\s+/g, '-').replace(/[^a-z0-9-]/g, '') };
 },
};

flosync.registerConnector('util.slugify', slugifyConnector);

const w = flosync.workflow('slugify-api')
 .http('POST', '/slugify')
 .step('slug', s => s.connector('util.slugify', 'slugify', { body: { text: v.body('title') } }))
 .step('respond', s => s.responder('json', { body: v.stepResult('slug') }))
 .build();
flosync.register(w);

const result = await flosync.run('slugify-api', { body: { title: 'Hello World!' } });

Sample 4: Full Flow (ConnectorBuilder + Workflow + Execute)#

const { FlosyncClient, connector, flosync, variable: v } = require('cliodot-flosync');

async function main() {
 const client = new FlosyncClient({
   baseUrl: process.env.FLOSYNC_BASE_URL || 'http://localhost:8080',
   apiKey: process.env.FLOSYNC_API_KEY,
   apiSecret: process.env.FLOSYNC_API_SECRET
 });
 await client.authenticate();

 const connectorDef = connector('weather-api', 'Weather API')
   .description('Weather data API')
   .baseUrl('https://api.weather.com/v1')
   .apiKey('')
   .get('Get Current', '/current', {
     params: { city: '{{ vars.city }}', units: 'metric' },
     response_mapping: { temp: 'temperature', humidity: 'humidity' }
   })
   .meta({
     category: 'Weather',
     logo: 'https://via.placeholder.com/64',
     status: 'active',
     visibility_settings: { visibility: 'private', public_access: 'none' }
   })
   .build();

 await client.connectors.push(connectorDef);
 await client.connectors.install('weather-api', {
   auth: { api_key: process.env.WEATHER_API_KEY }
 });

 const result = await client.connectors.execute('weather-api', 'Get Current', {
   vars: { city: 'London' }
 });
 console.log(result);
}
  • /docs/workflows – Creating and running workflows via SDK
  • /docs/connectors/api-reference – Auth types, visibility, schema reference
  • /docs/connectors/build#method-4-postman-conversion – Postman → connector conversion
  • /docs/connectors – Available connectors and usage

Method 5: Decorator Connectors#

For ultimate flexibility, you can define your own Connectors using class-based decorators instead of using the generic defineCustomConnector pattern.

Use the @ConnectorClass decorator and map endpoints instantly without rewriting HTTP loops or complex REST logic!

import { ConnectorClass, ActionEndpoint, ActionCustom, buildConnectorFromClass, flosync } from "cliodot";

// You can pass configuration mimicking flosync REST definition payloads directly into the class decorator:
@ConnectorClass("paystack", "Paystack Integrations", {
 type: "REST",
 base_url: "https://api.paystack.co",
 auth: { type: "bearer", token: process.env.PAYSTACK_SECRET_KEY }
})
export class Paystack {
  // Handled entirely by Flosync's underlying REST connector automatically!
 @ActionEndpoint("initializeTransaction", "POST", "/transaction/initialize", {
    headers: { "Content-Type": "application/json" }
 })
 initializeTransaction() {}

 // Paths parameter resolution happens completely implicitly via the REST connector.
 @ActionEndpoint("verifyTransaction", "GET", "/transaction/verify/:reference")
 verifyTransaction() {}
  // Or, run a completely custom code execution if you need SDKs or Database integrations
 @ActionCustom("generateSignature")
 async generateSignature(payload: any) {
    const crypto = require("crypto");
    return { signature: crypto.createHmac('sha512', process.env.SECRET).update(payload.body.data).digest('hex') };
 }
}

// Convert class into standard flosync connector schema
export const paystackConfig = buildConnectorFromClass(Paystack);

export const PaystackConnector = {
 id: PaystackImplementation.id,
 actions: PaystackImplementation.actions
};

// Register it natively just like any built-in module
flosync.registerConnector(paystackConfig.id, paystackConfig);

Authentication Options

Flosync supports built-in, natively executed REST authentication blocks inside the @ConnectorClass config object. You do not need to write boilerplate header injection code!

  1. Bearer Token (Default)
auth: { type: "bearer", token: "your-token", header_name: "Authorization", prefix: "Bearer " }
  1. API Key
auth: { type: "api_key", api_key: "your-api-key", key_name: "x-api-key" }
  1. No Auth
auth: { type: "none" }

Then hook it right into your workflows! v.body("email") passes safely into the initialization engine directly!

import { Workflow, Http, Connector, Responder, v } from "cliodot";

@Workflow("payment-initialize", "Initialize Payment")
@Http("POST", "/payment/initialize")
export class PaymentInitializeWorkflow {
 @Connector(
   PaystackConnector.id,
   PaystackConnector.actions.initializeTransaction,
   {
     body: {
       email: v.body("email"),
       amount: v.body("amount"),
       reference: v.body("reference"),
     }
   },
   { order: 0 }
 )
 initialize() {}

 @Responder("json", { body: { message: "Payment initialized successfully", data: v.stepResult("initialize") } }, { order: 1 })
 respond() {}
}

Custom execute connector plus decorators (catalog sample)#

For behavior that is not a REST template (in-memory demo data, your own SDK, legacy SOAP adapter, etc.), register a connector with an execute function, then call it from decorators with @Connector exactly like a built-in connector. This mirrors the carrier.store pattern: actions such as listProducts and createProduct, options.body / options.params / options.pathParams passed from the step config.

Define typed action names and the connector:

import { flosync, defineCustomConnector } from "cliodot";

type CatalogProduct = { id: string; name: string; price: number };

const Catalog = defineCustomConnector("acme.catalog", {
 listProducts: "listProducts",
 createProduct: "createProduct",
});

const catalogProducts: CatalogProduct[] = [
 { id: "p_static_1", name: "Starter", price: 0 },
 { id: "p_static_2", name: "Pro", price: 49 },
];

flosync.registerConnector(Catalog.id, {
 _id: Catalog.id,
 type: "system",
 name: "Acme Catalog",
 meta: { category: "catalog" },
 auth: { type: "none" },
 execute: async (action, options) => {
   const body = (options.body ?? {}) as Record<string, unknown>;
   const params = (options.params ?? {}) as Record<string, unknown>;
   const pathParams = (options.pathParams ?? {}) as Record<string, unknown>;
   const merged = { ...params, ...pathParams };

   if (action === Catalog.actions.listProducts) {
     const limitRaw = merged.limit;
     const limit =
       limitRaw === undefined || limitRaw === ""
         ? catalogProducts.length
         : Math.max(0, Number(limitRaw));
     const slice = catalogProducts.slice(0, Number.isFinite(limit) ? limit : catalogProducts.length);
     return { ok: true, products: slice, count: slice.length };
   }

   if (action === Catalog.actions.createProduct) {
     const name = String(body.name ?? "").trim();
     const price = Number(body.price);
     if (!name || Number.isNaN(price)) {
       return { ok: false, error: "name_and_price_required" };
     }
     const id = `p_${Date.now()}`;
     const product: CatalogProduct = { id, name, price };
     catalogProducts.push(product);
     return { ok: true, product };
   }

   return { ok: false, error: "unknown_action" };
 },
});

List products with decorator stage: "post" to shape the payload (add price_display, clamp list) before the responder runs:

import {
 flosync,
 v,
 Workflow,
 Http,
 Connector,
 Responder,
 buildWorkflowFromClass,
} from "cliodot";

@Workflow("acme-catalog-list-decorators", "Catalog list (decorators)")
@Http("GET", "/decorators/catalog/products")
class CatalogListDecoratorsWorkflow {
 @Connector(
   Catalog.id,
   Catalog.actions.listProducts,
   { params: { limit: v.query("limit") } },
   { order: 0, stage: "post" }
 )
 list(ctx: WorkflowContext<{ products?: CatalogProduct[]; [k: string]: any }>): StepOutput<{ products: any[]; count: number }> {
   const input = ctx?.input ?? {};
   const items = Array.isArray(input.products) ? input.products : [];
   const withLabels = items.map((p) => ({
     ...p,
     price_display: `$${Number(p.price).toFixed(2)}`,
   }));
   return { out: { ...input, products: withLabels, count: withLabels.length } };
 }

 @Responder(
   "json",
   {
     body: {
       ok: true,
       count: v.stepResult("list", "count"),
       products: v.stepResult("list", "products"),
     },
   },
   { order: 1 }
 )
 respond() {}
}

Create product with the same connector; the method body flags bad input and normalizes the successful response:

@Workflow("acme-catalog-create-decorators", "Catalog create (decorators)")
@Http("POST", "/decorators/catalog/products")
class CatalogCreateDecoratorsWorkflow {
 @Connector(
   Catalog.id,
   Catalog.actions.createProduct,
   { body: { name: v.body("name"), price: v.body("price") } },
   { order: 0, stage: "post" }
 )
 save(ctx: WorkflowContext<{ ok?: boolean; product?: CatalogProduct; [k: string]: any }>): StepOutput<any> {
   const input = ctx?.input ?? {};
   if (!input.ok) return { out: input };
   const p = input.product as CatalogProduct;
   return {
     out: {
       ...input,
       product: { ...p, price_display: `$${Number(p.price).toFixed(2)}` },
     },
   };
 }

 @Responder(
   "json",
   {
     statusCode: 201,
     body: {
       ok: true,
       product: v.stepResult("save", "product"),
     },
   },
   { order: 1 }
 )
 respond() {}
}

Example POST /decorators/catalog/products JSON body:

{
 "name": "Enterprise",
 "price": 199
}

Example 201 response body:

{
 "ok": true,
 "product": {
   "id": "p_1711286400000",
   "name": "Enterprise",
   "price": 199,
   "price_display": "$199.00"
 }
}

Example GET /decorators/catalog/products?limit=1 response body:

{
 "ok": true,
 "count": 1,
 "products": [
   { "id": "p_static_1", "name": "Starter", "price": 0, "price_display": "$0.00" }
 ]
}

Register both workflows with buildWorkflowFromClass / flosync.register or flosync.registerFromClass. Use flosync.run("<workflow-id>", { body: { ... } }) with the @Workflow string ID (the engine also registers the same workflow under __rawId when you use kebab-case IDs).