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);
}
Related Documentation#
/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!
- Bearer Token (Default)
auth: { type: "bearer", token: "your-token", header_name: "Authorization", prefix: "Bearer " }
- API Key
auth: { type: "api_key", api_key: "your-api-key", key_name: "x-api-key" }
- 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).