cliodot 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +23 -1628
  2. package/dist/AuthAppClient.d.ts +29 -0
  3. package/dist/AuthAppClient.d.ts.map +1 -0
  4. package/dist/AuthAppClient.js +1 -0
  5. package/dist/ConnectorBuilder.js +1 -1
  6. package/dist/Flosync.js +1 -1
  7. package/dist/FlosyncClient.js +1 -1
  8. package/dist/FunctionBuilder.js +1 -1
  9. package/dist/OAuthAppClient.d.ts.map +1 -1
  10. package/dist/OAuthAppClient.js +1 -1
  11. package/dist/ValidatorBuilder.js +1 -1
  12. package/dist/WorkflowBuilder.js +1 -1
  13. package/dist/__tests__/OAuthAppClient.test.js +1 -1
  14. package/dist/__tests__/function.test.js +1 -1
  15. package/dist/__tests__/parse-api-error.test.js +1 -1
  16. package/dist/__tests__/runner.test.js +1 -1
  17. package/dist/connectors/builtin.js +1 -1
  18. package/dist/connectors/registry.js +1 -1
  19. package/dist/decorators/build.js +1 -1
  20. package/dist/decorators/connector.js +1 -1
  21. package/dist/decorators/index.js +1 -1
  22. package/dist/decorators/metadata.js +1 -1
  23. package/dist/decorators/steps.js +1 -1
  24. package/dist/decorators/workflow.js +1 -1
  25. package/dist/errors.js +1 -1
  26. package/dist/http/parse-api-error.js +1 -1
  27. package/dist/http/sanitize-execution-headers.js +1 -1
  28. package/dist/index.d.ts +4 -2
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +1 -1
  31. package/dist/runner/ProcessorEngine.js +1 -1
  32. package/dist/runner/connector.executor.js +1 -1
  33. package/dist/runner/utilities/math.executor.js +1 -1
  34. package/dist/runner/utilities/random.executor.js +1 -1
  35. package/dist/runner/utilities/string.executor.js +1 -1
  36. package/dist/template.js +1 -1
  37. package/dist/transformers/StepsToNodesTransformer.js +1 -1
  38. package/dist/transformers/WorkflowTransformer.js +1 -1
  39. package/dist/types/auth-app.api.d.ts +171 -0
  40. package/dist/types/auth-app.api.d.ts.map +1 -0
  41. package/dist/types/auth-app.api.js +1 -0
  42. package/dist/types/builtin.js +1 -1
  43. package/dist/types/client.api.js +1 -1
  44. package/dist/types/connector.js +1 -1
  45. package/dist/types/context.js +1 -1
  46. package/dist/types/function.js +1 -1
  47. package/dist/types/index.js +1 -1
  48. package/dist/types/oauth-app.api.d.ts +102 -2
  49. package/dist/types/oauth-app.api.d.ts.map +1 -1
  50. package/dist/types/oauth-app.api.js +1 -1
  51. package/dist/types/validator.js +1 -1
  52. package/dist/types/workflow.js +1 -1
  53. package/dist/validators/validators.js +1 -1
  54. package/dist/variable.js +1 -1
  55. package/package.json +4 -1
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # cliodot
2
2
 
3
- SDK for building workflows, APIs, and functions with Cliodot Flowsync. Define workflows and functions in code, run them locally, and optionally sync with your Flowsync account.
3
+ SDK for building workflows, APIs, and functions with Cliodot Flowsync. Define workflows and functions in code, run them locally, and sync with your Cliodot account.
4
+
5
+ **Full documentation:** [docs.cliodot.com](https://docs.cliodot.com)
4
6
 
5
7
  ## Installation
6
8
 
@@ -8,49 +10,25 @@ SDK for building workflows, APIs, and functions with Cliodot Flowsync. Define wo
8
10
  npm install cliodot
9
11
  ```
10
12
 
11
- ### Module Support
12
-
13
- Cliodot supports both **ES Modules** (recommended for Node.js 16+, TypeScript, and modern frameworks) and **CommonJS**.
13
+ **ES Modules / TypeScript**
14
14
 
15
- **ES Modules (ESM) / TypeScript**
16
15
  ```javascript
17
16
  import { flosync, v } from 'cliodot';
18
- // or: import { flosync, variable as v } from 'cliodot';
19
- ```
20
-
21
- **CommonJS (CJS)**
22
- ```javascript
23
- const { flosync, v } = require('cliodot');
24
- // or: const { flosync, variable: v } = require('cliodot');
25
17
  ```
26
18
 
27
- **Note for some ESM environments:**
28
- If you encounter `SyntaxError: Named export 'flosync' not found`, use the default import instead:
19
+ **CommonJS**
29
20
 
30
21
  ```javascript
31
- import pkg from 'cliodot';
32
- const { flosync, v } = pkg;
33
- ```
34
-
35
- Optional peer dependencies:
36
-
37
- ```bash
38
- npm install mongodb mysql2
39
- ```
40
-
41
- For PostgreSQL and Redis:
42
-
43
- ```bash
44
- npm install pg ioredis
22
+ const { flosync, v } = require('cliodot');
45
23
  ```
46
24
 
47
- For password hashing (Argon2, bcrypt):
25
+ Optional peer dependencies (install only what you use):
48
26
 
49
27
  ```bash
50
- npm install argon2 bcrypt
28
+ npm install mongodb mysql2 pg ioredis argon2 bcrypt
51
29
  ```
52
30
 
53
- ## Quick Start
31
+ ## Quick start
54
32
 
55
33
  ```javascript
56
34
  import { flosync, v } from 'cliodot';
@@ -72,1619 +50,36 @@ flosync.register(w);
72
50
  const result = await flosync.run('create-order', { body: { item: 'Widget', qty: 2 } });
73
51
  ```
74
52
 
75
- ### Run Payload
76
-
77
- Pass a `body` (or plain object) as the trigger; the engine treats it as `trigger.body` and `trigger.data`. You can also pass:
78
-
79
- | Field | Description |
80
- |-------|-------------|
81
- | `body` / `data` | Request body; use `v.body('account')`, `v.body('email')` |
82
- | `headers` | Simulated HTTP headers for the run (`v.header('x-api-key')`); merged with `executionHeaders` (see below) |
83
- | `executionHeaders` | Extra headers merged after `headers` (same sanitization); useful to separate trigger data from auth headers |
84
- | `pathParams` | Route params (`v.pathParam('id')`) |
85
- | `params` | Extra params (`v.param('page')`) |
86
- | `query` | Query string params (`v.query('filter')`) |
87
- | `vars` | Override workflow vars (`v.vars('apiKey')`) |
88
- | `envVars` / `env` | Environment overrides (`v.env('MONGO_URI')`) |
89
-
90
- ```javascript
91
- flosync.run('my-workflow', { body: { id: 1 } });
92
- flosync.run('my-workflow', { body: { id: 1 }, headers: { 'x-request-id': 'abc' }, pathParams: { id: '123' }, query: { page: 1 } });
93
- ```
94
-
95
- ### Execution headers (local and remote)
96
-
97
- Workflow steps read `state.headers` (for example `v.header('authorization')`). You control what appears there via the run payload and, for remote runs, the client options below.
98
-
99
- Local runs (`flosync.run` with a registered workflow): `headers` and `executionHeaders` on the payload are merged (after sanitization) into `state.headers`. You can also pass `executionHeaders` as the third argument to `flosync.run`, which merges on top of the payload.
100
-
101
- ```javascript
102
- await flosync.run('my-workflow', { body: { id: 1 }, headers: { 'x-request-id': 'a' } }, {
103
- executionHeaders: { authorization: `Bearer ${integrationToken}` },
104
- });
105
- ```
106
-
107
- Remote runs (`flosync.run` / `flosync.runById` with `apiKey` and `apiSecret`, or `FlosyncClient`): the SDK sends its own `Authorization` header to the Cliodot API (session JWT). That value is not copied into the workflow `state.headers`, since it is not the same credential your integration steps expect.
108
-
109
- To give the server workflow the bearer or other headers it was designed for, pass them explicitly:
110
-
111
- - In the payload: `headers` or `executionHeaders` on the object you pass to `run` / `runById` (both are merged into the test payload the API receives).
112
- - On the client: `client.workflows.run(groupId, triggerId, { payload, executionHeaders })` — `executionHeaders` is sent in the JSON body and merged last on the server.
113
- - Webhook test: `client.workflows.runByWebhook(path, method, { body, executionHeaders })`.
114
-
115
- Merge order on the server is: sanitized real incoming HTTP headers (transport), then `payload.headers`, then `executionHeaders` from the body. Later keys win. Cookies, hop-by-hop headers, `sec-*`, `proxy-*`, and `proxy-authorization` are dropped from user-supplied maps. `authorization` is allowed in `headers` / `executionHeaders` so you can supply an integration bearer; it is still stripped from the raw request line so the Cliodot session token does not overwrite your value.
116
-
117
- `sanitizeExecutionHeaders` is exported if you want to preview what will be sent; the same rules apply on the server.
118
-
119
- Public webhooks (unauthenticated URL): the server does not apply an `executionHeaders` field from the JSON body for merging. Use normal request headers or put values in the webhook payload and map them in the workflow.
120
-
121
53
  ## Configuration
122
54
 
123
55
  ```javascript
124
56
  flosync.configure({
125
57
  apiKey: process.env.FLOSYNC_API_KEY,
126
58
  apiSecret: process.env.FLOSYNC_API_SECRET,
127
- baseUrl: 'http://localhost:8080' //optional,
59
+ baseUrl: process.env.CLIODOT_BASE_URL,
128
60
  projectId: process.env.FLOSYNC_PROJECT_ID,
129
- debug: false, // Enable to include stepResults in every response
130
61
  connectors: {
131
62
  'mongodb.system': { uri: process.env.MONGO_URI, database: 'myapp' },
132
- 'mysql.system': { uri: process.env.MYSQL_URI },
133
- 'postgres.system': { uri: process.env.PG_URI, database: 'myapp' },
134
- 'redis.system': { url: process.env.REDIS_URL },
135
- 'cliodot': { apiKey: process.env.CLIODOT_API_KEY },
136
- },
137
- cliodot: { baseUrl: 'https://api.example.com', apiKey: process.env.CLIODOT_API_KEY },
138
- });
139
- ```
140
-
141
- `baseUrl` defaults to `http://localhost:8080` when omitted. For remote runs (workflows, functions, connectors), only `apiKey` and `apiSecret` are required. Set `CLIODOT_BASE_URL` (or `baseUrl`) to match your main Cliodot API server port.
142
-
143
- ## Workflows
144
-
145
- ### Triggers (optional)
146
-
147
- Triggers are optional. If you wrap workflows in your own HTTP handler, omit `.http()` and call `flosync.run()` with your payload.
148
-
149
- | Method | Description |
150
- |--------|-------------|
151
- | `.http(method, path)` | HTTP trigger (e.g. `POST /orders`) |
152
- | `.job(cron?, timezone?)` | Job/cron trigger (e.g. `'0 * * * *'`, `'UTC'`) |
153
- | `.schedule(cron, timezone?)` | Alias for scheduled jobs |
154
- | `.webhook(method, path)` | Alias for `.http()` |
155
-
156
- ```javascript
157
- flosync.workflow('api-order').http('POST', '/orders').step(...).build();
158
- flosync.workflow('daily-job').job('0 0 * * *', 'UTC').step(...).build();
159
- flosync.workflow('manual').step(...).build();
160
- ```
161
-
162
- ### Variable references
163
-
164
- Use `variable` (or `v`) instead of typing `{{ }}`:
165
-
166
- | Method | Example | Resolves to |
167
- |--------|---------|-------------|
168
- | `v.body(...path)` | `v.body('amount')`, `v.body('user', 'email')` | `{{ body.amount }}`, `{{ body.user.email }}` |
169
- | `v.trigger(...path)` | `v.trigger('email')` | `{{ trigger.email }}` |
170
- | `v.stepResult(stepId, ...path)` | `v.stepResult('charge', 'id')` | `{{ stepResults.charge.id }}` |
171
- | `v.header(key)` | `v.header('x-api-key')` | `{{ headers["x-api-key"] }}` |
172
- | `v.param(key)` | `v.param('page')` | `{{ params["page"] }}` |
173
- | `v.pathParam(key)` | `v.pathParam('id')` | `{{ pathParams["id"] }}` |
174
- | `v.query(key)` | `v.query('filter')` | `{{ query["filter"] }}` |
175
- | `v.vars(key)` | `v.vars('apiKey')` | `{{ vars["apiKey"] }}` |
176
- | `v.env(key)` | `v.env('MONGO_URI')` | `{{ env["MONGO_URI"] }}` |
177
- | `v.args(...path)` | `v.args('amount')` | `{{ args.amount }}` |
178
- | `v.expr(expression)` | `v.expr('length(body.items) * 10')` | `{{ length(body.items) * 10 }}` |
179
-
180
- ```javascript
181
- import { flosync, v } from 'cliodot';
182
-
183
- s.validator({ amount: v.body('amount'), email: v.body('email') });
184
- s.transform({ total: v.stepResult('calculate', 'total'), id: v.stepResult('charge', 'id') });
185
- ```
186
-
187
- ### Typed `stepResult` paths (`defineStepVars`)
188
-
189
- `v.stepResult(stepId, ...pathParts)` treats every segment as a plain `string`, so your editor cannot suggest valid fields from a connector response or from another step’s output.
190
-
191
- Use **`defineStepVars`** to describe, once per workflow file, a map from **step id** (the decorated **method name**) to that step’s **result type**. The returned **`stepResult`** then constrains the first argument to those keys and each following argument to the next key along a valid path into that type (nested objects supported up to a fixed depth). Import **`ConnectorResponse`** (and your **`defineTypedConnector`** value) so connector steps reuse the same response shape you already declared for typing.
192
-
193
- ```typescript
194
- import { defineStepVars, v, defineTypedConnector } from "cliodot";
195
- import type { ConnectorResponse } from "cliodot";
196
-
197
- const Api = defineTypedConnector("my-api", { pay: "pay" }, {
198
- pay: {
199
- body: {} as { amount: number },
200
- response: { status: "", data: { id: "", link: "" } },
201
- },
202
- });
203
-
204
- const steps = defineStepVars<{
205
- pay: ConnectorResponse<typeof Api, "pay">;
206
- validate: { errors?: Record<string, unknown> };
207
- }>();
208
-
209
- steps.stepResult("pay", "data", "link");
210
- steps.stepResult("validate", "errors");
211
- ```
212
-
213
- The type **`StepKeyPath<T>`** is exported if you want to reuse the path-tuple logic elsewhere. Plain **`v.stepResult`** remains available for ad hoc strings and for code that does not need completions.
214
-
215
- ### Class decorators (`buildWorkflowFromClass`)
216
-
217
- For `@Connector`, you can target a connector that exists only on Flowsync in three ways: pass the published connector id and action as strings (`@Connector('my-connector', 'charge', { installation_id: '...', connector_version: '1.2.0' })`); use `defineCustomConnector` / `defineCustomConnectorFromList` with that same id; or use `defineRemoteConnectorRef('my-connector', { pay: 'charge' })` and `@Connector(MyRef, 'pay', config)` for a clearer name. With `apiKey` and `apiSecret` set, the local runner loads REST definitions through `client.connectors.get` and respects `connector_version` on the step when fetching.
218
-
219
- **`@CallWorkflow` by webhook path** — Use `@CallWorkflow({ webhookPath: '/orders', webhookMethod: 'POST' }, payload)` where `webhookPath` and `webhookMethod` match the child workflow’s HTTP trigger (same values as `@Http('POST', '/orders')` on that workflow). You do **not** pass a workflow group id for this mode: the platform finds the child by path and method for the tenant. When the local runner calls Flowsync with `apiKey` / `apiSecret`, it uses **`POST /workflows/test/webhook`** with a JSON body `{ webhookPath, webhookMethod, payload, ... }` (`client.workflows.runByWebhook`). On the server, `call_workflow` resolves the child the same way as a real webhook (including route patterns) and merges path params into the child’s `pathParams`. The second decorator argument is the child payload; optional fields on that object include `environment` (`dev` / `prod`), `executionHeaders`, and `remote: true` to skip the in-memory registry and always hit the API. `webhookPath` / `webhookMethod` may be templated with `{{ }}` and are rendered before lookup.
220
-
221
- **`@CallWorkflow` by group id** — `@CallWorkflow('my-group-id', payload)` runs the child via **`POST /workflows/:groupId/test/trigger/:triggerId`** (see `client.workflows.run`). Use this when you target a specific group and trigger explicitly. You can combine `workflow_id` with a webhook path in the serialized step only when you need both; the path-only flow above stays independent of group ids.
222
-
223
- ### Step Types
224
-
225
- - `connector(id, action, config)` - REST/API call (optional `connector_version` in `config` for versioned tenant connectors when syncing workflows to Flowsync)
226
- - `db(engine, action, config)` - MongoDB, MySQL, PostgreSQL, or Redis (optional `connector_version` when relevant)
227
- - `validator(fields)` - Validate input
228
- - `condition(expr)` - Branch on expression (use `.then()` and `.else()`)
229
- - `transform(mapping)` - Map or transform data
230
- - `call(slug, args)` - Invoke a function
231
- - `auth(connectorId, action, config)` - Authentication
232
- - `util(connectorId, action, config)` - Utility (see [Utilities](#utilities))
233
- - `encrypt(connectorId, action, config)` - Encryption (see [Encryption](#encryption))
234
- - `responder(type, config)` - Return response (json, http, raw, redirect, empty)
235
- - `log(message)` - Log message
236
- - `delay(ms)` - Delay execution
237
-
238
- ### Branching
239
-
240
- Steps that can succeed or fail support `.then()` and `.else()`:
241
-
242
- ```javascript
243
- .step('validate', s => s.validator({ amount: '{{ trigger.body.amount }}' })
244
- .then('process')
245
- .else('error'))
246
- .step('check', s => s.condition('{{ stepResults.validate.amount > 0 }}')
247
- .then('create')
248
- .else('invalid'))
249
- ```
250
-
251
- ### Decorator Workflows
252
-
253
- You can define workflows using TypeScript decorators instead of the fluent `.workflow().step()` builder.
254
-
255
- 1. Enable decorators in your consuming project's `tsconfig.json`:
256
-
257
- ```json
258
- {
259
- "compilerOptions": {
260
- "experimentalDecorators": true
261
- }
262
- }
263
- ```
264
-
265
- 2. Define a workflow class and decorate it with `@Workflow()` and `@Http()` (or `@Job()` / `@Schedule()`).
266
-
267
- 3. Decorate class methods to create steps. By default, the step ID is the method name.
268
-
269
- 4. Build and register the workflow with `buildWorkflowFromClass()` or `flosync.registerFromClass()`.
270
-
271
- 5. Optional: add JavaScript inside the decorated method body.
272
-
273
- If a decorated method contains non-empty code, the SDK uses it based on the step decorator option `stage`:
274
-
275
- - `stage: "pre"` (default): injects an extra `code` step right before the decorated step. The injected code receives `ctx` with:
276
-
277
- - `ctx.input` (previous step output, or trigger data for the first step)
278
- - `ctx.trigger` (trigger payload)
279
- - `ctx.steps` (all previous `stepResults`)
280
- - `ctx.vars` (workflow vars, including anything set via `setVars`)
281
-
282
- To pass values to later steps, return an object with `setVars`:
283
-
284
- ```typescript
285
- return { setVars: { limit: 10 } };
286
- ```
287
-
288
- Later templates can read them via `v.vars("limit")` (resolves to `{{ vars["limit"] }}`) or `{{ vars.limit }}`.
289
-
290
- - `stage: "post"`: executes the method body after the decorated step finishes, and allows overriding the decorated step output directly.
291
-
292
- In this mode, return:
293
-
294
- ```typescript
295
- return { out: <newOutput>, setVars?: { ... } };
296
- ```
297
-
298
- The overridden output becomes the value of `v.stepResult("<stepId>")` for subsequent steps.
299
-
300
- In `stage: "post"`, `ctx.input` is the decorated step output produced by the connector/function call.
301
-
302
- #### `stage: "post"` in depth
303
-
304
- `post` is the hook that runs **after** the decorated step’s primary work finishes. For `@Connector`, `@Db`, and other steps that call out to a runtime implementation, the sequence is: resolve inputs → run the connector or database action (or built-in executor) → take the returned payload as the step output → **then** run your method body with `ctx.input` set to that output. Anything you return as `{ out }` replaces what gets stored in `stepResults` for that step id, so the rest of the workflow sees your transformed value. Use `setVars` in the same return value to publish workflow variables without changing the step output. Failures and HTTP error handling still apply to the primary step; the post hook is for normalization (mapping provider DTOs to your domain shape), redacting fields, enriching derived properties, or turning awkward API responses into a stable contract for later steps and responders. `@Step` defaults to `stage: "post"` so standalone code steps behave like “compute output from prior context” without injecting an extra predecessor code step.
305
-
306
- ### `WorkflowContext<TInput, TSteps, TVars>`
307
-
308
- Instead of using `ctx: any`, you can type the context object passed to step methods. Import `WorkflowContext` from `cliodot`:
309
-
310
- ```typescript
311
- import type { WorkflowContext } from "cliodot";
312
- ```
313
-
314
- `WorkflowContext` is generic with three optional type parameters:
315
-
316
- | Parameter | Default | Description |
317
- |-----------|---------|-------------|
318
- | `TInput` | `Record<string, any>` | Shape of `ctx.input` (previous step output or trigger data) |
319
- | `TSteps` | `Record<string, any>` | Shape of `ctx.steps` (accumulated step results) |
320
- | `TVars` | `Record<string, any>` | Shape of `ctx.vars` (workflow-level variables) |
321
-
322
- The full context object includes:
323
-
324
- | Property | Type | Description |
325
- |----------|------|-------------|
326
- | `input` | `TInput` | Input data for this step |
327
- | `trigger` | `any` | Raw trigger payload |
328
- | `steps` | `TSteps` | All step results keyed by step ID |
329
- | `vars` | `TVars` | Workflow variables (read/write via `setVars`) |
330
- | `headers` | `Record<string, string>` | HTTP headers from the request |
331
- | `params` | `Record<string, any>` | Merged query + route params |
332
- | `pathParams` | `Record<string, any>` | URL path parameters |
333
- | `query` | `Record<string, any>` | Query string parameters |
334
- | `body` | `any` | Raw request body |
335
- | `env` | `Record<string, string>` | Environment variables |
336
-
337
- The interface also has an index signature (`[key: string]: any`) so users can attach custom properties.
338
-
339
- ### `StepOutput<T>`
340
-
341
- The return type for step methods that produce output. Import from `cliodot`:
342
-
343
- ```typescript
344
- import type { StepOutput } from "cliodot";
345
- ```
346
-
347
- ```typescript
348
- export interface StepOutput<T = any> {
349
- /** The output payload — stored in stepResults[stepId] */
350
- out: T;
351
- /** Optional: update workflow variables */
352
- setVars?: Record<string, any>;
353
- }
354
- ```
355
-
356
- Usage:
357
-
358
- ```typescript
359
- @Connector(MyStore.id, MyStore.actions.list, {}, { order: 0, stage: "post" })
360
- list(ctx: WorkflowContext<{ items: Item[] }>): StepOutput<{ sorted: Item[] }> {
361
- const sorted = [...ctx.input.items].sort((a, b) => a.id - b.id);
362
- return { out: { sorted } };
363
- }
364
- ```
365
-
366
- > **Note:** `StepOutput` is different from the `StepResult` exported by `WorkflowBuilder` (which is the builder-pattern return type with `.then()` / `.else()` chaining). Use `StepOutput` for decorator workflow method return types.
367
-
368
- ### `@Step` — Custom Step Decorator
369
-
370
- `@Step` is a generic decorator for writing custom workflow step logic without needing to pair it with a connector, database, or other built-in step type. The method body is extracted at build time and executed as a code step with access to the full `WorkflowContext`.
371
-
372
- ```typescript
373
- import { Workflow, Http, Step, Responder, buildWorkflowFromClass, v } from "cliodot";
374
- import type { WorkflowContext, StepOutput } from "cliodot";
375
- ```
376
-
377
- **Options** (same as other step decorators):
378
-
379
- | Option | Type | Default | Description |
380
- |--------|------|---------|-------------|
381
- | `id` | `string` | method name | Custom step ID |
382
- | `order` | `number` | auto | Execution order |
383
- | `stage` | `"pre" \| "post"` | `"post"` | When the method body runs |
384
- | `then` | `string` | — | Step ID to jump to on success |
385
- | `else` | `string` | — | Step ID to jump to on failure |
386
-
387
- **Basic example:**
388
-
389
- ```typescript
390
- @Workflow("greet-user", "Greet User")
391
- @Http("POST", "/greet")
392
- class GreetWorkflow {
393
- @Step({ order: 0 })
394
- greet(ctx: WorkflowContext<{ name: string }>): StepOutput<{ greeting: string }> {
395
- const name = ctx.input.name || "World";
396
- return { out: { greeting: `Hello, ${name}!` } };
397
- }
398
-
399
- @Responder("json", { body: { message: v.stepResult("greet", "greeting") } }, { order: 1 })
400
- respond() {}
401
- }
402
-
403
- export const greetWorkflow = buildWorkflowFromClass(GreetWorkflow);
404
- ```
405
-
406
- **With `setVars` and branching:**
407
-
408
- ```typescript
409
- @Workflow("process-order", "Process Order")
410
- @Http("POST", "/orders/process")
411
- class ProcessOrderWorkflow {
412
- @Step({ order: 0, then: "finalize", else: "reject" })
413
- validate(ctx: WorkflowContext<{ amount: number; currency: string }>): StepOutput<{ valid: boolean }> {
414
- const { amount, currency } = ctx.input;
415
- const valid = amount > 0 && ["USD", "EUR", "GBP"].includes(currency);
416
- return {
417
- out: { valid },
418
- setVars: { processedAt: new Date().toISOString() },
419
- };
420
- }
421
-
422
- @Responder("json", { body: { ok: true, processedAt: v.vars("processedAt") } }, { order: 1 })
423
- finalize() {}
424
-
425
- @Responder("json", { statusCode: 400, body: { ok: false, error: "Invalid order" } }, { order: 2 })
426
- reject() {}
427
- }
428
- ```
429
-
430
- **Combining `@Step` with other decorators:**
431
-
432
- `@Step` can be freely combined with `@Connector`, `@Db`, `@Condition`, `@Responder`, etc. in the same workflow class:
433
-
434
- ```typescript
435
- @Workflow("mixed-workflow", "Mixed Workflow")
436
- @Http("POST", "/mixed")
437
- class MixedWorkflow {
438
- @Step({ order: 0 })
439
- prepare(ctx: WorkflowContext<{ email: string }>): StepOutput<{ normalized: string }> {
440
- return { out: { normalized: ctx.input.email.toLowerCase().trim() } };
441
- }
442
-
443
- @Db("mongodb", "findOne", { collection: "users", query: { email: v.stepResult("prepare", "normalized") } }, { order: 1 })
444
- findUser() {}
445
-
446
- @Condition(v.expr("stepResults.findUser != null"), { then: "found", else: "notFound", order: 2 })
447
- check() {}
448
-
449
- @Responder("json", { body: { user: v.stepResult("findUser") } }, { order: 3 })
450
- found() {}
451
-
452
- @Responder("json", { statusCode: 404, body: { error: "User not found" } }, { order: 4 })
453
- notFound() {}
454
- }
455
- ```
456
-
457
- ### Decorator example: list todos with typed context
458
-
459
- ```typescript
460
- import { flosync, v, Workflow, Http, Connector, Condition, Responder, buildWorkflowFromClass, defineCustomConnector } from "cliodot";
461
- import type { WorkflowContext, StepOutput } from "cliodot";
462
-
463
- const CarrierStore = defineCustomConnector("carrier.store", {
464
- listTodos: "listTodos",
465
- });
466
-
467
- interface TodoItem {
468
- title?: string;
469
- completed?: boolean;
470
- createdAt?: string;
471
- [key: string]: any;
472
- }
473
-
474
- interface ListInput {
475
- todos?: TodoItem[];
476
- [key: string]: any;
477
- }
478
-
479
- interface ListOutput {
480
- todos: Array<TodoItem & { title_upper: string; completed_label: string }>;
481
- completedTodos: TodoItem[];
482
- pendingTodos: TodoItem[];
483
- stats: { total: number; completed: number; pending: number };
484
- }
485
-
486
- @Workflow("todos-list-decorators", "Todos List (Decorators)")
487
- @Http("GET", "/todos")
488
- class TodosListDecoratorsWorkflow {
489
- @Connector(
490
- CarrierStore.id,
491
- CarrierStore.actions.listTodos,
492
- { params: { limit: 10 } },
493
- { order: 0, stage: "post" }
494
- )
495
- list(ctx: WorkflowContext<ListInput>): StepOutput<ListOutput> {
496
- const input = ctx?.input ?? {};
497
- const todos = Array.isArray(input.todos) ? input.todos : [];
498
- const sorted = [...todos].sort((a, b) =>
499
- new Date(b?.createdAt ?? 0).getTime() - new Date(a?.createdAt ?? 0).getTime()
500
- );
501
- const completedTodos = sorted.filter((t) => Boolean(t?.completed));
502
- const pendingTodos = sorted.filter((t) => !Boolean(t?.completed));
503
-
504
- const decoratedTodos = sorted.map((t) => {
505
- const title = String(t?.title ?? "").trim();
506
- return {
507
- ...t,
508
- title,
509
- title_upper: title.toUpperCase(),
510
- completed_label: t?.completed ? "done" as const : "todo" as const,
511
- };
512
- });
513
-
514
- return {
515
- out: {
516
- ...input,
517
- todos: decoratedTodos,
518
- completedTodos,
519
- pendingTodos,
520
- stats: {
521
- total: sorted.length,
522
- completed: completedTodos.length,
523
- pending: pendingTodos.length,
524
- },
525
- },
526
- };
527
- }
528
-
529
- @Condition(v.stepResult("list", "valid"), { then: "ok", else: "unauth", order: 1 })
530
- check() {}
531
-
532
- @Responder("json", { body: { todos: v.stepResult("list", "todos") } }, { order: 2 })
533
- ok() {}
534
-
535
- @Responder("json", { statusCode: 401, body: { error: "Unauthorized" } }, { order: 3 })
536
- unauth() {}
537
- }
538
-
539
- const workflow = buildWorkflowFromClass(TodosListDecoratorsWorkflow);
540
- flosync.register(workflow);
541
- ```
542
-
543
- If you prefer direct registration:
544
-
545
- ```typescript
546
- flosync.registerFromClass(TodosListDecoratorsWorkflow);
547
- ```
548
-
549
- ### Decorator workflow: validation, MongoDB, string utilities, shaped JSON
550
-
551
- This pattern matches what many APIs do: validate input, derive a slug with `utility.string`, write to MongoDB, return a single JSON object with nested `note` metadata (not a flat dump of raw step outputs).
552
-
553
- Configure the database once:
554
-
555
- ```javascript
556
- flosync.configure({
557
- connectors: {
558
- "mongodb.system": { uri: process.env.MONGO_URI, database: "myapp" },
559
63
  },
560
64
  });
561
65
  ```
562
66
 
563
- Workflow (imports: `flosync`, `v`, `Workflow`, `Http`, `Validator`, `Util`, `Db`, `Responder`, `buildWorkflowFromClass`, `ConnectorId`):
564
-
565
- ```typescript
566
- import {
567
- flosync,
568
- v,
569
- Workflow,
570
- Http,
571
- Validator,
572
- Util,
573
- Db,
574
- Responder,
575
- buildWorkflowFromClass,
576
- ConnectorId,
577
- } from "cliodot";
578
-
579
- @Workflow("notes-create-decorators", "Notes create (decorators)")
580
- @Http("POST", "/decorators/notes")
581
- class NotesCreateDecoratorsWorkflow {
582
- @Validator(
583
- { title: v.body("title"), body: v.body("body") },
584
- { order: 0, then: "slugify", else: "validationError" }
585
- )
586
- validate() {}
587
-
588
- @Util(
589
- ConnectorId.Utility.String,
590
- "slugify",
591
- { string: v.body("title") },
592
- { order: 1 }
593
- )
594
- slugify() {}
595
-
596
- @Db(
597
- "mongodb",
598
- "insertOne",
599
- {
600
- collection: "notes",
601
- document: {
602
- title: v.body("title"),
603
- body: v.body("body"),
604
- slug: v.stepResult("slugify", "value"),
605
- },
606
- },
607
- { order: 2 }
608
- )
609
- persist() {}
610
-
611
- @Responder(
612
- "json",
613
- {
614
- statusCode: 201,
615
- body: {
616
- ok: true,
617
- note: {
618
- id: v.stepResult("persist", "insertedId"),
619
- title: v.body("title"),
620
- slug: v.stepResult("slugify", "value"),
621
- body: v.body("body"),
622
- },
623
- },
624
- },
625
- { order: 3 }
626
- )
627
- created() {}
628
-
629
- @Responder(
630
- "json",
631
- {
632
- statusCode: 400,
633
- body: {
634
- ok: false,
635
- error: "validation_failed",
636
- details: v.stepResult("validate", "errors"),
637
- },
638
- },
639
- { order: 4 }
640
- )
641
- validationError() {}
642
- }
643
-
644
- const notesDecoratorsWorkflow = buildWorkflowFromClass(NotesCreateDecoratorsWorkflow);
645
- flosync.register(notesDecoratorsWorkflow);
646
- ```
647
-
648
- Example request:
649
-
650
- ```http
651
- POST /decorators/notes
652
- Content-Type: application/json
653
-
654
- {
655
- "title": "Ship checklist",
656
- "body": "Pack boxes, print labels, hand off to carrier."
657
- }
658
- ```
659
-
660
- Example success response body:
661
-
662
- ```json
663
- {
664
- "ok": true,
665
- "note": {
666
- "id": "65a1b2c3d4e5f6789012345",
667
- "title": "Ship checklist",
668
- "slug": "ship-checklist",
669
- "body": "Pack boxes, print labels, hand off to carrier."
670
- }
671
- }
672
- ```
673
-
674
- Example validation failure response body:
675
-
676
- ```json
677
- {
678
- "ok": false,
679
- "error": "validation_failed",
680
- "details": {
681
- "title": "Field is required"
682
- }
683
- }
684
- ```
685
-
686
- `@Util` is typed the same way as `s.util(ConnectorId.Utility.String, "slugify", { ... })` in the fluent builder. `@Db` supports the `mongodb` and `mysql` engines (see [Database](#database)). Step IDs in `v.stepResult("slugify", "value")` refer to the **method name** you put on each decorated step (after sanitization, dots and hyphens in custom IDs become underscores).
687
-
688
- ### Decorator Connectors
689
-
690
- For ultimate flexibility, you can define your own **Connectors** using class-based decorators instead of using the generic `defineCustomConnector` pattern.
691
-
692
- Use the `@ConnectorClass` decorator and map endpoints instantly without rewriting HTTP loops or complex REST logic!
693
-
694
- ```typescript
695
- import { ConnectorClass, ActionEndpoint, ActionCustom, buildConnectorFromClass, flosync } from "cliodot";
696
-
697
- // You can pass configuration mimicking flosync REST definition payloads directly into the class decorator:
698
- @ConnectorClass("paystack", "Paystack Integrations", {
699
- type: "REST",
700
- base_url: "https://api.paystack.co",
701
- auth: { type: "bearer", token: process.env.PAYSTACK_SECRET_KEY }
702
- })
703
- export class Paystack {
704
-
705
- // Handled entirely by Flosync's underlying REST connector automatically!
706
- @ActionEndpoint("initializeTransaction", "POST", "/transaction/initialize", {
707
- headers: { "Content-Type": "application/json" }
708
- })
709
- initializeTransaction() {}
710
-
711
- // Paths parameter resolution happens completely implicitly via the REST connector.
712
- @ActionEndpoint("verifyTransaction", "GET", "/transaction/verify/:reference")
713
- verifyTransaction() {}
714
-
715
- // Or, run a completely custom code execution if you need SDKs or Database integrations
716
- @ActionCustom("generateSignature")
717
- async generateSignature(payload: any) {
718
- const crypto = require("crypto");
719
- return { signature: crypto.createHmac('sha512', process.env.SECRET).update(payload.body.data).digest('hex') };
720
- }
721
- }
722
-
723
- // Convert class into standard flosync connector schema
724
- export const paystackConfig = buildConnectorFromClass(Paystack);
725
-
726
- export const PaystackConnector = {
727
- id: PaystackImplementation.id,
728
- actions: PaystackImplementation.actions
729
- };
730
-
731
- // Register it natively just like any built-in module
732
- flosync.registerConnector(paystackConfig.id, paystackConfig);
733
- ```
734
-
735
- #### Authentication Options
736
-
737
- Flosync supports built-in, natively executed REST authentication blocks inside the `@ConnectorClass` config object. You do not need to write boilerplate header injection code!
738
-
739
- 1. **Bearer Token (*Default*)**
740
- ```typescript
741
- auth: { type: "bearer", token: "your-token", header_name: "Authorization", prefix: "Bearer " }
742
- ```
743
- 2. **API Key**
744
- ```typescript
745
- auth: { type: "api_key", api_key: "your-api-key", key_name: "x-api-key" }
746
- ```
747
- 3. **No Auth**
748
- ```typescript
749
- auth: { type: "none" }
750
- ```
751
-
752
- Then hook it right into your workflows! This example uses `@Validator` for type-safe input checking, generates a unique payment reference using the built-in `utility.random` connector, and passes it to the Paystack API:
753
-
754
- ```typescript
755
- import { Workflow, Http, Connector, Responder, Validator, v } from "cliodot";
756
-
757
- @Workflow("payment-initialize", "Initialize Payment")
758
- @Http("POST", "/payment/initialize")
759
- export class PaymentInitializeWorkflow {
760
-
761
- @Validator([
762
- {
763
- fields: [v.body("email")],
764
- validators: [{ name: "required" }, { name: "is_email" }]
765
- },
766
- {
767
- fields: [v.body("amount")],
768
- validators: [{ name: "required" }]
769
- }
770
- ], { order: 0, then: "generateReference", else: "invalid" })
771
- validate() {}
772
-
773
- @Responder("json", {
774
- statusCode: 400,
775
- body: {
776
- ok: false,
777
- message: "Validation failed",
778
- errors: v.stepResult("validate", "errors")
779
- }
780
- }, { order: 1 })
781
- invalid() {}
782
-
783
- @Connector("utility.random", "random_uuid", {}, { order: 2 })
784
- generateReference() {}
785
-
786
- @Connector(
787
- PaystackConnector.id,
788
- PaystackConnector.actions.initializeTransaction,
789
- {
790
- body: {
791
- email: v.body("email"),
792
- amount: v.body("amount"),
793
- reference: v.stepResult("generateReference", "value"),
794
- }
795
- },
796
- { order: 3 }
797
- )
798
- initialize() {}
799
-
800
- @Responder("json", { body: { message: "Payment initialized successfully", data: v.stepResult("initialize") } }, { order: 4 })
801
- respond() {}
802
- }
803
- ```
804
-
805
- ### Custom `execute` connector plus decorators (catalog sample)
806
-
807
- 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.
808
-
809
- Define typed action names and the connector:
810
-
811
- ```typescript
812
- import { flosync, defineCustomConnector } from "cliodot";
813
-
814
- type CatalogProduct = { id: string; name: string; price: number };
815
-
816
- const Catalog = defineCustomConnector("acme.catalog", {
817
- listProducts: "listProducts",
818
- createProduct: "createProduct",
819
- });
820
-
821
- const catalogProducts: CatalogProduct[] = [
822
- { id: "p_static_1", name: "Starter", price: 0 },
823
- { id: "p_static_2", name: "Pro", price: 49 },
824
- ];
825
-
826
- flosync.registerConnector(Catalog.id, {
827
- _id: Catalog.id,
828
- type: "system",
829
- name: "Acme Catalog",
830
- meta: { category: "catalog" },
831
- auth: { type: "none" },
832
- execute: async (action, options) => {
833
- const body = (options.body ?? {}) as Record<string, unknown>;
834
- const params = (options.params ?? {}) as Record<string, unknown>;
835
- const pathParams = (options.pathParams ?? {}) as Record<string, unknown>;
836
- const merged = { ...params, ...pathParams };
837
-
838
- if (action === Catalog.actions.listProducts) {
839
- const limitRaw = merged.limit;
840
- const limit =
841
- limitRaw === undefined || limitRaw === ""
842
- ? catalogProducts.length
843
- : Math.max(0, Number(limitRaw));
844
- const slice = catalogProducts.slice(0, Number.isFinite(limit) ? limit : catalogProducts.length);
845
- return { ok: true, products: slice, count: slice.length };
846
- }
847
-
848
- if (action === Catalog.actions.createProduct) {
849
- const name = String(body.name ?? "").trim();
850
- const price = Number(body.price);
851
- if (!name || Number.isNaN(price)) {
852
- return { ok: false, error: "name_and_price_required" };
853
- }
854
- const id = `p_${Date.now()}`;
855
- const product: CatalogProduct = { id, name, price };
856
- catalogProducts.push(product);
857
- return { ok: true, product };
858
- }
859
-
860
- return { ok: false, error: "unknown_action" };
861
- },
862
- });
863
- ```
864
-
865
- List products with decorator `stage: "post"` to shape the payload (add `price_display`, clamp list) before the responder runs:
866
-
867
- ```typescript
868
- import {
869
- flosync,
870
- v,
871
- Workflow,
872
- Http,
873
- Connector,
874
- Responder,
875
- buildWorkflowFromClass,
876
- } from "cliodot";
877
-
878
- @Workflow("acme-catalog-list-decorators", "Catalog list (decorators)")
879
- @Http("GET", "/decorators/catalog/products")
880
- class CatalogListDecoratorsWorkflow {
881
- @Connector(
882
- Catalog.id,
883
- Catalog.actions.listProducts,
884
- { params: { limit: v.query("limit") } },
885
- { order: 0, stage: "post" }
886
- )
887
- list(ctx: WorkflowContext<{ products?: CatalogProduct[]; [k: string]: any }>): StepOutput<{ products: any[]; count: number }> {
888
- const input = ctx?.input ?? {};
889
- const items = Array.isArray(input.products) ? input.products : [];
890
- const withLabels = items.map((p) => ({
891
- ...p,
892
- price_display: `$${Number(p.price).toFixed(2)}`,
893
- }));
894
- return { out: { ...input, products: withLabels, count: withLabels.length } };
895
- }
67
+ For remote runs against your Cliodot account, set `apiKey` and `apiSecret`. `baseUrl` defaults to `http://localhost:8080` when omitted.
896
68
 
897
- @Responder(
898
- "json",
899
- {
900
- body: {
901
- ok: true,
902
- count: v.stepResult("list", "count"),
903
- products: v.stepResult("list", "products"),
904
- },
905
- },
906
- { order: 1 }
907
- )
908
- respond() {}
909
- }
910
- ```
911
-
912
- Create product with the same connector; the method body flags bad input and normalizes the successful response:
913
-
914
- ```typescript
915
- @Workflow("acme-catalog-create-decorators", "Catalog create (decorators)")
916
- @Http("POST", "/decorators/catalog/products")
917
- class CatalogCreateDecoratorsWorkflow {
918
- @Connector(
919
- Catalog.id,
920
- Catalog.actions.createProduct,
921
- { body: { name: v.body("name"), price: v.body("price") } },
922
- { order: 0, stage: "post" }
923
- )
924
- save(ctx: WorkflowContext<{ ok?: boolean; product?: CatalogProduct; [k: string]: any }>): StepOutput<any> {
925
- const input = ctx?.input ?? {};
926
- if (!input.ok) return { out: input };
927
- const p = input.product as CatalogProduct;
928
- return {
929
- out: {
930
- ...input,
931
- product: { ...p, price_display: `$${Number(p.price).toFixed(2)}` },
932
- },
933
- };
934
- }
935
-
936
- @Responder(
937
- "json",
938
- {
939
- statusCode: 201,
940
- body: {
941
- ok: true,
942
- product: v.stepResult("save", "product"),
943
- },
944
- },
945
- { order: 1 }
946
- )
947
- respond() {}
948
- }
949
- ```
950
-
951
- Example `POST /decorators/catalog/products` JSON body:
952
-
953
- ```json
954
- {
955
- "name": "Enterprise",
956
- "price": 199
957
- }
958
- ```
959
-
960
- Example `201` response body:
961
-
962
- ```json
963
- {
964
- "ok": true,
965
- "product": {
966
- "id": "p_1711286400000",
967
- "name": "Enterprise",
968
- "price": 199,
969
- "price_display": "$199.00"
970
- }
971
- }
972
- ```
973
-
974
- Example `GET /decorators/catalog/products?limit=1` response body:
975
-
976
- ```json
977
- {
978
- "ok": true,
979
- "count": 1,
980
- "products": [
981
- { "id": "p_static_1", "name": "Starter", "price": 0, "price_display": "$0.00" }
982
- ]
983
- }
984
- ```
985
-
986
- 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).
987
-
988
- ## Functions
989
-
990
- ```javascript
991
- const validatePayment = flosync.function('validate-payment')
992
- .input({ amount: 'number', currency: 'string' })
993
- .step('validate', s => s.validator({ amount: '{{ args.amount }}', currency: '{{ args.currency }}' }))
994
- .step('format', s => s.transform({ valid: '{{ stepResults.validate.valid }}', amount: '{{ args.amount }}' }))
995
- .outputFrom('format')
996
- .build();
997
-
998
- flosync.registerFunction(validatePayment);
999
-
1000
- const result = await flosync.runFunction('validate-payment', { amount: 100, currency: 'USD' });
1001
- ```
1002
-
1003
- ## Run by ID (remote)
1004
-
1005
- When configured with `apiKey`, `apiSecret`, and `baseUrl`, you can run workflows and functions by ID without registering them locally. They are assumed to exist on your account.
1006
-
1007
- ```javascript
1008
- flosync.configure({ apiKey, apiSecret });
1009
-
1010
- await flosync.run('workflow-id-from-account', { body: { amount: 100 } });
1011
- await flosync.runById('workflow-id', { body: { ... } }, { triggerId: 'trigger' });
1012
- await flosync.runByWebhook('/orders', 'POST', { body: { ... } });
1013
-
1014
- await flosync.runFunction('function-id-from-account', { amount: 100 });
1015
- await flosync.runFunctionById('function-id', { amount: 100 });
1016
- ```
1017
-
1018
- If a workflow/function is registered locally, `run` / `runFunction` use the local version. If not found and the client is configured, they run remotely. Pass `{ remote: false }` to disable remote fallback.
1019
-
1020
- ### Connector versioning (REST connectors)
1021
-
1022
- Tenant REST connectors can have **published semver snapshots**. The SDK and API use that version when loading connector definitions for workflow runs and for direct execute calls.
1023
-
1024
- **Workflow steps (fluent builder and `@Connector` config)**
1025
- Include an optional semver string on the step config so pushed workflows pin the same snapshot the runner expects:
1026
-
1027
- ```typescript
1028
- s.connector("paystack_conn_id", "initializeTransaction", {
1029
- connector_version: "1.2.0",
1030
- body: { email: v.body("email"), amount: v.body("amount") },
1031
- });
1032
- ```
1033
-
1034
- Event triggers can pin the source connector the same way on the workflow object: `trigger.source_connector_id` and `trigger.source_connector_version` (see types in `IWorkflowTrigger`).
1035
-
1036
- **`flosync.runConnector` (remote)**
1037
- Pass `connector_version` in the options object. Non-owners must match their **installed** version; owners may use other published versions or `draft` per API rules.
1038
-
1039
- ```typescript
1040
- await flosync.runConnector(Paystack, "initializeTransaction", {
1041
- connector_version: "1.2.0",
1042
- body: { email: "a@b.com", amount: 100 },
1043
- });
1044
- ```
1045
-
1046
- **`FlosyncClient`** (after `configure` with `apiKey` / `apiSecret`):
1047
-
1048
- - `client.connectors.get(connectorId, { semver: "1.2.0" })` or `{ version: "1.2.0" }` — same query params as the Flowsync GET connector API.
1049
- - `client.connectors.install(connectorId, { auth, base_url, version: "latest" })` — `version` is optional (body or query on the server).
1050
- - `client.connectors.execute(connectorId, action, { connector_version: "1.2.0", body, params, ... })` — sent as `connector_version` on the execute request body.
1051
-
1052
- Workflows built with `WorkflowBuilder` / decorators serialize `connector_version` onto nodes when you `workflows.push`, so pulls round-trip the field.
1053
-
1054
- ## Database
1055
-
1056
- Use `s.db(engine, action, config)` or `s.connector(connectorId, action, config)` with built-in database connectors.
1057
-
1058
- | Connector | Engine | Actions |
1059
- |-----------|--------|---------|
1060
- | `mongodb.system` | mongodb | insertOne, insertMany, findOne, find, updateOne, deleteOne |
1061
- | `mysql.system` | mysql | query |
1062
- | `postgres.system` | postgres | query |
1063
- | `redis.system` | redis | redis.get, redis.set, redis.delete, redis.hset, redis.hget, redis.lpush, redis.lpop, redis.zadd, redis.create_hash, redis.delete_key |
1064
-
1065
- ```javascript
1066
- s.db('mongodb', 'insertOne', { collection: 'orders', document: v.body() });
1067
- s.db('mysql', 'query', { query: 'SELECT * FROM users WHERE id = ?', params: [v.body('id')] });
1068
- s.db('postgres', 'query', { query: 'SELECT * FROM users', params: [] });
1069
- s.db('redis', 'redis.set', { key: 'cache:user:1', value: v.stepResult('fetch'), ttl: 3600 });
1070
- ```
1071
-
1072
- Configure in `flosync.configure({ connectors: { 'postgres.system': { uri, database }, 'redis.system': { url } } })`.
1073
-
1074
- ## Utilities
1075
-
1076
- Use `s.util(connectorId, action, config)` with built-in utility connectors. Use `ConnectorId.Utility` and `ConnectorActions` or `getConnectorActions(connectorId)` for autocomplete and suggestions.
1077
-
1078
- | Connector | Actions |
1079
- |-----------|---------|
1080
- | `utility.date_time` | get_current_date, get_current_time, get_current_datetime, format, format_date, parse_date, add_time, subtract_time, diff_between_dates, start_of_day, end_of_day, start_of_week, end_of_week, start_of_month, end_of_month, start_of_year, end_of_year, convert_timezone, get_timezone, list_timezones, is_leap_year, get_unix_timestamp, from_unix_timestamp |
1081
- | `utility.string` | concat, split, join, replace, replace_all, trim, trim_start, trim_end, to_uppercase, to_lowercase, capitalize, substring, length, contains, starts_with, ends_with, remove_whitespace, slugify, pad_start, pad_end, reverse, encode_base64, decode_base64, encode_uri, decode_uri, after, after_last, before, before_last, between, between_first, camel, char_at, chop_start, chop_end, contains_all, doesnt_contain, doesnt_start_with, doesnt_end_with, deduplicate, excerpt, finish, start, headline, is, is_ascii, is_json, is_ulid, is_url, is_uuid, is_empty, is_match, kebab, snake, studly, title, lcfirst, ucfirst, ucwords, ucsplit, limit, mask, match, match_all, pad_both, plural, singular, position, random, remove, repeat, replace_array, replace_first, replace_last, replace_matches, replace_start, replace_end, squish, substr, substr_count, substr_replace, swap, take, word_count, word_wrap, words, wrap, unwrap, uuid, ulid, strip_tags, ascii, apa, class_basename |
1082
- | `utility.math` | add, subtract, multiply, divide, modulo, power, round, floor, ceil, abs, min, max, average, sum, clamp, random_between, percentage, percentage_of, sqrt, log |
1083
- | `utility.random` | random_int, random_float, random_string, random_uuid, uuid, random_boolean, random_choice, random_password, random_hex, random_alphanumeric, random_date, random_color, shuffle_array |
1084
- | `utility.compare` | diff_objects, diff_arrays, compare_strings, compare_numbers, compare_dates, is_changed, is_equal, is_not_equal, is_greater_than, is_less_than, is_between, is_empty, is_null_or_undefined, is_same_type, deep_equal, shallow_equal, similarity_score, array_intersection, array_union, array_difference |
1085
- | `utility.geo` | calculate_distance, point_in_polygon, get_bounding_box, reverse_geocode, geocode, convert_coordinates |
1086
-
1087
- ```javascript
1088
- import { ConnectorId, ConnectorActions, getConnectorActions } from 'cliodot';
1089
-
1090
- s.util(ConnectorId.Utility.String, 'slugify', { string: 'Hello World' });
1091
- s.util(ConnectorId.Utility.Math, 'clamp', { number: 150, min: 0, max: 100 });
1092
- s.util(ConnectorId.Utility.Compare, 'diff_objects', { object1: v.stepResult('prev'), object2: v.body() });
1093
- s.util(ConnectorId.Utility.Geo, 'calculate_distance', { lat1: 0, lon1: 0, lat2: 1, lon2: 1 });
1094
-
1095
- const stringActions = getConnectorActions(ConnectorId.Utility.String);
1096
- ```
1097
-
1098
- ## Responders
1099
-
1100
- Use `s.responder(type, config)` with built-in responder types.
1101
-
1102
- | Type | Description |
1103
- |------|-------------|
1104
- | json | JSON response (default content-type: application/json) |
1105
- | http | Same as json, supports custom headers |
1106
- | raw | Plain text with custom contentType |
1107
- | redirect | 302/301 redirect with Location header |
1108
- | empty | 202 with no body |
1109
-
1110
- ```javascript
1111
- s.responder('json', { body: v.stepResult('data') });
1112
- s.responder('raw', { body: 'OK', contentType: 'text/plain' });
1113
- s.responder('redirect', { location: 'https://example.com', statusCode: 301 });
1114
- s.responder('empty', { statusCode: 202 });
1115
- ```
1116
-
1117
- ## Validation
1118
-
1119
- The `@Validator` decorator provides type-safe input validation for workflow steps. Validators use a `ValidationRule` union type that gives you full IDE auto-completion for all available rules and their configuration options.
1120
-
1121
- ### Basic usage
1122
-
1123
- ```typescript
1124
- import { Validator, v } from "cliodot";
1125
-
1126
- @Validator([
1127
- {
1128
- fields: [v.body("email")],
1129
- validators: [{ name: "required" }, { name: "is_email" }]
1130
- },
1131
- {
1132
- fields: [v.body("amount")],
1133
- validators: [{ name: "required" }, { name: "is_number" }]
1134
- }
1135
- ], { order: 0, then: "process", else: "error" })
1136
- validate() {}
1137
- ```
1138
-
1139
- ### Available validators
1140
-
1141
- | Validator | Config | Description |
1142
- |-----------|--------|-------------|
1143
- | `required` | `{ message? }` | Field must be present and non-null |
1144
- | `not_empty` | `{ message? }` | Field must not be empty |
1145
- | `is_empty` | `{ message? }` | Field must be empty |
1146
- | `is_email` | `{ message? }` | Valid email format |
1147
- | `is_number` | `{ message? }` | Must be a number |
1148
- | `is_string` | `{ message? }` | Must be a string |
1149
- | `is_boolean` | `{ message? }` | Must be a boolean |
1150
- | `is_array` | `{ message? }` | Must be an array |
1151
- | `is_object` | `{ message? }` | Must be an object |
1152
- | `is_url` | `{ message? }` | Valid URL format |
1153
- | `is_date` | `{ message? }` | Valid date format |
1154
- | `is_uuid` | `{ message? }` | Valid UUID (v1-v5) |
1155
- | `is_cuid` | `{ message? }` | Valid CUID |
1156
- | `is_jwt` | `{ message? }` | Valid JWT structure (`header.payload.signature`) |
1157
- | `is_json` | `{ message? }` | Valid parseable JSON string |
1158
- | `is_alphanumeric` | `{ message? }` | Letters and numbers only |
1159
- | `is_alpha` | `{ message? }` | Letters only |
1160
- | `is_numeric_string` | `{ message? }` | Numeric digits only |
1161
- | `is_hex_color` | `{ message? }` | Valid hex color (`#FFF` or `#123456`) |
1162
- | `is_base64` | `{ message? }` | Valid base64 encoded string |
1163
- | `is_credit_card` | `{ message? }` | Valid credit card (Luhn algorithm) |
1164
- | `is_lowercase` | `{ message? }` | Must be all lowercase |
1165
- | `is_uppercase` | `{ message? }` | Must be all uppercase |
1166
- | `is_slug` | `{ message? }` | Valid URL slug (`my-cool-slug`) |
1167
- | `is_mac_address` | `{ message? }` | Valid MAC address |
1168
- | `is_port` | `{ message? }` | Valid TCP/UDP port (0-65535) |
1169
- | `is_currency` | `{ message? }` | Valid currency format |
1170
- | `is_latitude` | `{ message? }` | Valid latitude (-90 to 90) |
1171
- | `is_longitude` | `{ message? }` | Valid longitude (-180 to 180) |
1172
- | `is_strong_password` | `{ minLength?, minLowercase?, minUppercase?, minNumbers?, minSymbols?, message? }` | Configurable password strength |
1173
- | `is_ip` | `{ version?: 4 \| 6, message? }` | Valid IPv4 or IPv6 address |
1174
- | `is_phone` | `{ pattern?, message? }` | Phone number format |
1175
- | `is_address` | `{ minLength?, maxLength?, message? }` | Address string checks |
1176
- | `contains` / `not_contains` | `{ search, caseSensitive?, message? }` | String contains/excludes a value |
1177
- | `begins_with` | `{ prefix, caseSensitive?, message? }` | String starts with prefix |
1178
- | `ends_with` | `{ suffix, caseSensitive?, message? }` | String ends with suffix |
1179
- | `is_in` / `not_in` | `{ values: any[], caseSensitive?, message? }` | Value is/isn't in a list |
1180
- | `length` | `{ min?, max?, exact?, message? }` | String/array length bounds |
1181
- | `range` | `{ min?, max?, message? }` | Numeric range bounds |
1182
- | `matches` | `{ pattern, flags?, message? }` | Regex pattern match |
1183
- | `equals` / `not_equals` | `{ value, message? }` | Strict equality check |
1184
- | `greater_than` / `less_than` | `{ value: number, message? }` | Numeric comparison |
1185
- | `greater_than_or_equal` / `less_than_or_equal` | `{ value: number, message? }` | Inclusive numeric comparison |
1186
-
1187
- ### Configurable validators
1188
-
1189
- Some validators accept rich configuration objects with IDE autocomplete:
1190
-
1191
- ```typescript
1192
- // Strong password with custom constraints
1193
- @Validator([
1194
- {
1195
- fields: [v.body("password")],
1196
- validators: [{
1197
- name: "is_strong_password",
1198
- config: { minLength: 12, minUppercase: 2, minSymbols: 1 }
1199
- }]
1200
- }
1201
- ], { order: 0, then: "register", else: "invalid" })
1202
- validatePassword() {}
1203
-
1204
- // IP address with version lock
1205
- @Validator([
1206
- {
1207
- fields: [v.body("server_ip")],
1208
- validators: [{ name: "is_ip", config: { version: 4 } }]
1209
- }
1210
- ], { order: 0, then: "connect", else: "invalid" })
1211
- validateIP() {}
1212
-
1213
- // Range + custom message
1214
- @Validator([
1215
- {
1216
- fields: [v.body("age")],
1217
- validators: [{
1218
- name: "range",
1219
- config: { min: 18, max: 120, message: "Age must be between 18 and 120" }
1220
- }]
1221
- }
1222
- ], { order: 0, then: "proceed", else: "invalid" })
1223
- validateAge() {}
1224
- ```
1225
-
1226
- ### Validation branching
1227
-
1228
- Use `then` and `else` to route the workflow based on validation results:
1229
-
1230
- ```typescript
1231
- @Validator([
1232
- { fields: [v.body("email")], validators: [{ name: "required" }, { name: "is_email" }] }
1233
- ], { order: 0, then: "process", else: "validationError" })
1234
- validate() {}
1235
-
1236
- @Responder("json", {
1237
- statusCode: 400,
1238
- body: { ok: false, errors: v.stepResult("validate", "errors") }
1239
- }, { order: 1 })
1240
- validationError() {}
1241
- ```
1242
-
1243
- When validation fails, the `errors` object is populated with field-level error messages that the responder can return to the client.
1244
-
1245
- ## Debug Mode
1246
-
1247
- Flosync includes a built-in debug mode that injects a `_debug` object into workflow responses, containing the full `stepResults` map of every step that executed. This is invaluable for development and troubleshooting without adding `console.log` statements.
1248
-
1249
- ### Enabling debug
1250
-
1251
- **1. Global (via config)** — applies to all workflow runs:
1252
-
1253
- ```typescript
1254
- flosync.configure({
1255
- debug: true,
1256
- connectors: { ... },
1257
- });
1258
- ```
1259
-
1260
- **2. Per-request (via header)** — enable for a single call:
1261
-
1262
- ```bash
1263
- curl -X POST http://localhost:3384/payment/initialize \
1264
- -H "Content-Type: application/json" \
1265
- -H "x-flosync-debug: true" \
1266
- -d '{"email": "user@example.com", "amount": 5000}'
1267
- ```
1268
-
1269
- **3. Per-request (via query param):**
1270
-
1271
- ```bash
1272
- curl "http://localhost:3384/books?debug=true"
1273
- ```
1274
-
1275
- ### Debug output
1276
-
1277
- When debug mode is active, a `_debug` key is appended to the response body:
1278
-
1279
- ```json
1280
- {
1281
- "message": "Payment initialized successfully",
1282
- "data": {
1283
- "authorization_url": "https://checkout.paystack.com/abc123",
1284
- "reference": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
1285
- },
1286
- "_debug": {
1287
- "stepResults": {
1288
- "validate": { "valid": true },
1289
- "generateReference": { "value": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" },
1290
- "initialize": {
1291
- "authorization_url": "https://checkout.paystack.com/abc123",
1292
- "reference": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
1293
- }
1294
- }
1295
- }
1296
- }
1297
- ```
1298
-
1299
- When debug mode is **off** (default), the `_debug` key is completely absent — zero overhead and no payload pollution.
1300
-
1301
- > **Note:** Debug mode works for both local workflow execution (`flosync.run()`) and remote execution via `client.workflows.run()` / `client.workflows.runByWebhook()`.
1302
-
1303
- ## Encryption
1304
-
1305
- Use `s.encrypt(connectorId, action, config)` with built-in encryption connectors. All use Node `crypto`; Argon2 and bcrypt require optional packages.
1306
-
1307
- | Connector | Actions |
1308
- |-----------|---------|
1309
- | `base64.encryption` | `base64.encode`, `base64.decode` |
1310
- | `hash.encryption` | `hash`, `hash.md5`, `hash.sha1`, `hash.sha256`, `hash.sha512` |
1311
- | `aes.encryption` | `aes.encrypt`, `aes.decrypt`, `aes.generate_key`, `aes.generate_iv` |
1312
- | `rsa.encryption` | `rsa.generate_keypair`, `rsa.encrypt`, `rsa.decrypt`, `rsa.sign`, `rsa.verify` |
1313
- | `hmac.encryption` | `hmac.create`, `hmac.verify` |
1314
- | `password.encryption` | `password.argon2_hash`, `password.argon2_verify`, `password.pbkdf2_hash`, `password.pbkdf2_verify`, `password.scrypt_hash`, `password.scrypt_verify`, `password.bcrypt_hash`, `password.bcrypt_verify`, `password.generate_salt` |
1315
-
1316
- **Examples:**
1317
-
1318
- ```javascript
1319
- s.encrypt('hash.encryption', 'hash', { data: v.body('payload'), algorithm: 'sha256' });
1320
- s.encrypt('base64.encryption', 'base64.encode', { data: v.stepResult('prev') });
1321
- s.encrypt('aes.encryption', 'aes.encrypt', { data: v.body('secret'), key: v.vars('key') });
1322
- s.encrypt('rsa.encryption', 'rsa.sign', { data: v.body('payload'), private_key: v.vars('privateKey') });
1323
- s.encrypt('hmac.encryption', 'hmac.create', { data: v.body('message'), key: v.vars('secret') });
1324
- s.encrypt('password.encryption', 'password.argon2_hash', { password: v.body('password') });
1325
- ```
1326
-
1327
- **Optional packages:** `password.argon2_hash` / `password.argon2_verify` need `argon2`; `password.bcrypt_hash` / `password.bcrypt_verify` need `bcrypt`. Install with `npm install argon2 bcrypt` if you use those actions.
1328
-
1329
- ## Connector Registry
1330
-
1331
- Use `ConnectorId` and `ConnectorActions` for built-in connectors, or `defineCustomConnector` for your own. This gives you autocomplete and avoids typos.
1332
-
1333
- ### Built-in connectors
1334
-
1335
- ```javascript
1336
- import { flosync, ConnectorId, ConnectorActions, getConnectorActions, listBuiltInConnectors } from 'cliodot';
1337
-
1338
- // Database
1339
- s.db('mongodb', 'insertOne', { collection: 'orders', document: {} });
1340
- s.db('mysql', 'query', { query: 'SELECT 1', params: [] });
1341
- s.db('postgres', 'query', { query: 'SELECT 1', params: [] });
1342
- s.db('redis', 'redis.set', { key: 'key', value: 'value' });
1343
-
1344
- // Auth
1345
- s.auth(ConnectorId.Auth.Bearer, ConnectorActions[ConnectorId.Auth.Bearer][0], { token: '{{ headers.authorization }}' });
1346
-
1347
- // Utility - use ConnectorActions or getConnectorActions for autocomplete
1348
- s.util(ConnectorId.Utility.DateTime, 'get_current_datetime', {});
1349
- s.util(ConnectorId.Utility.String, 'slugify', { string: 'Hello World' });
1350
- s.util(ConnectorId.Utility.Math, 'add', { numbers: [10, 20] });
1351
- s.util(ConnectorId.Utility.Random, 'random_uuid', {});
1352
- s.util(ConnectorId.Utility.Compare, 'diff_objects', { object1: {}, object2: {} });
1353
- s.util(ConnectorId.Utility.Geo, 'calculate_distance', { lat1: 0, lon1: 0, lat2: 1, lon2: 1 });
1354
-
1355
- // Get actions for a connector (for suggestions/autocomplete)
1356
- const stringActions = getConnectorActions(ConnectorId.Utility.String);
1357
-
1358
- // List all built-in connectors and their actions
1359
- console.log(listBuiltInConnectors());
1360
- ```
1361
-
1362
- ### Custom connectors
1363
-
1364
- Define your connector with typed actions for autocomplete:
1365
-
1366
- ```javascript
1367
- import { defineCustomConnector } from 'cliodot';
1368
-
1369
- const MyStore = defineCustomConnector('my.store', {
1370
- login: 'login',
1371
- listItems: 'listItems',
1372
- createItem: 'createItem',
1373
- });
1374
-
1375
- flosync.registerConnector(MyStore.id, myStoreConnector);
1376
-
1377
- s.connector(MyStore.id, MyStore.actions.login, { body: { email: '{{ body.email }}' } });
1378
- s.connector(MyStore.id, MyStore.actions.listItems, {});
1379
- ```
1380
-
1381
- For a simple list of action names:
1382
-
1383
- ```javascript
1384
- import { defineCustomConnectorFromList } from 'cliodot';
1385
-
1386
- const MyStore = defineCustomConnectorFromList('my.store', ['login', 'listItems', 'createItem']);
1387
- ```
1388
-
1389
- ### Typed connectors (for remote connectors)
1390
-
1391
- When using remote connectors, you do not have the implementation locally. `defineTypedConnector` lets you define per-action schema so TypeScript can infer request/response shapes across decorators, builder calls, and `runConnector`.
1392
-
1393
- ```typescript
1394
- import { defineTypedConnector } from "cliodot";
1395
-
1396
- const Paystack = defineTypedConnector("paystack", {
1397
- initializeTransaction: "Initialize Transaction",
1398
- verifyTransaction: "Verify Transaction",
1399
- listTransactions: "List Transactions",
1400
- }, {
1401
- initializeTransaction: {
1402
- body: {} as { email: string; amount: number; reference?: string; callback_url?: string },
1403
- headers: {} as { "Idempotency-Key"?: string },
1404
- vars: {} as { traceId?: string },
1405
- response: {} as { authorization_url: string; access_code: string; reference: string },
1406
- },
1407
- verifyTransaction: {
1408
- pathParams: {} as { reference: string },
1409
- response: {} as { status: string; amount: number; currency: string; paid_at: string },
1410
- },
1411
- listTransactions: {
1412
- params: {} as { page?: number; perPage?: number; status?: string },
1413
- response: {} as { data: Array<{ reference: string; amount: number; status: string }> },
1414
- },
1415
- });
1416
- ```
1417
-
1418
- Schema fields:
1419
-
1420
- | Field | Description |
1421
- |-------|-------------|
1422
- | `body` | Shape of request body |
1423
- | `params` | Shape of query/params |
1424
- | `pathParams` | Shape of URL path params |
1425
- | `headers` | Shape of custom headers |
1426
- | `vars` | Shape of step-level vars merged before execution |
1427
- | `response` | Shape of expected response |
1428
-
1429
- Use `{} as YourType` to define each shape without runtime values.
1430
-
1431
- **Extracting types with helper utilities:**
1432
-
1433
- ```typescript
1434
- import type {
1435
- ConnectorBody,
1436
- ConnectorParams,
1437
- ConnectorPathParams,
1438
- ConnectorHeaders,
1439
- ConnectorVars,
1440
- ConnectorResponse,
1441
- } from "cliodot";
1442
-
1443
- type InitBody = ConnectorBody<typeof Paystack, "initializeTransaction">;
1444
- type InitHeaders = ConnectorHeaders<typeof Paystack, "initializeTransaction">;
1445
- type InitVars = ConnectorVars<typeof Paystack, "initializeTransaction">;
1446
- type VerifyPath = ConnectorPathParams<typeof Paystack, "verifyTransaction">;
1447
- type ListParams = ConnectorParams<typeof Paystack, "listTransactions">;
1448
- type VerifyResponse = ConnectorResponse<typeof Paystack, "verifyTransaction">;
1449
- ```
1450
-
1451
- **Using in decorators (typed action key + typed config):**
1452
-
1453
- ```typescript
1454
- import { Workflow, Http, Connector, Responder, v } from "cliodot";
1455
- import type { WorkflowContext, StepOutput, ConnectorResponse } from "cliodot";
1456
-
1457
- type InitResponse = ConnectorResponse<typeof Paystack, "initializeTransaction">;
1458
-
1459
- @Workflow("paystack-init", "Initialize Paystack Payment")
1460
- @Http("POST", "/pay")
1461
- class PaystackInitWorkflow {
1462
- @Connector(
1463
- Paystack,
1464
- "initializeTransaction",
1465
- {
1466
- body: { email: v.body("email"), amount: v.body("amount"), callback_url: v.body("callback_url") },
1467
- headers: { "Idempotency-Key": "{{ trigger.idempotencyKey }}" },
1468
- vars: { traceId: "{{ trigger.traceId }}" },
1469
- },
1470
- { order: 0, stage: "post" }
1471
- )
1472
- init(ctx: WorkflowContext<InitResponse>): StepOutput<{ url: string; ref: string }> {
1473
- return {
1474
- out: {
1475
- url: ctx.input.authorization_url,
1476
- ref: ctx.input.reference,
1477
- },
1478
- };
1479
- }
1480
-
1481
- @Responder("json", {
1482
- body: { url: v.stepResult("init", "url"), reference: v.stepResult("init", "ref") },
1483
- }, { order: 1 })
1484
- respond() {}
1485
- }
1486
- ```
1487
-
1488
- You can still use the classic style (`Paystack.id` + `Paystack.actions.someAction`) if you prefer.
1489
-
1490
- **Typed connectors also work in builder and direct runs:**
1491
-
1492
- ```typescript
1493
- // Builder
1494
- workflow.step("init", (s) =>
1495
- s.connector(Paystack, "initializeTransaction", {
1496
- body: { email: "{{ trigger.email }}", amount: "{{ trigger.amount }}" },
1497
- headers: { "Idempotency-Key": "{{ trigger.idempotencyKey }}" },
1498
- })
1499
- );
1500
-
1501
- // Direct connector execution
1502
- await flosync.runConnector(Paystack, "verifyTransaction", {
1503
- pathParams: { reference: "ref_123" },
1504
- });
1505
- ```
1506
-
1507
- Template strings are accepted in typed request fields (`body`, `params`, `pathParams`, `headers`, `vars`) so workflow expressions continue to work with strong typing.
1508
-
1509
- Available type helpers:
1510
-
1511
- | Helper | Description |
1512
- |--------|-------------|
1513
- | `ConnectorBody<TDef, TAction>` | Extracts `body` type |
1514
- | `ConnectorParams<TDef, TAction>` | Extracts `params` type |
1515
- | `ConnectorPathParams<TDef, TAction>` | Extracts `pathParams` type |
1516
- | `ConnectorHeaders<TDef, TAction>` | Extracts `headers` type |
1517
- | `ConnectorVars<TDef, TAction>` | Extracts `vars` type |
1518
- | `ConnectorResponse<TDef, TAction>` | Extracts `response` type |
1519
- | `ConnectorActionRequest<TDef, TAction>` | Extracts full request shape |
1520
- | `ConnectorTypedRequestConfig<TDef, TAction>` | Typed decorator/builder request config |
1521
-
1522
- Examples-first guide:
1523
-
1524
- - [`Typed Connector End-to-End Samples`](./TYPED_CONNECTOR_EXAMPLES.md)
1525
-
1526
- ## Connectors by ID
1527
-
1528
- Use `s.connector(connectorId, action, config)` with any connector ID. For remote runs, connectors are resolved from your account. For local runs, register connectors first with `flosync.registerConnector(id, connector)` or pass config in `configure({ connectors: { 'connector-id': { ... } } })`.
1529
-
1530
- ## API Client
1531
-
1532
- When configured with `apiKey`, `apiSecret`, and `baseUrl`:
1533
-
1534
- ```javascript
1535
- const client = flosync.getClient();
1536
-
1537
- await flosync.push(workflow);
1538
- const wf = await flosync.pull(groupId);
1539
-
1540
- const { workflowGroups, pagination } = await client.workflows.list({ status: 'active', page: 1, limit: 20 });
1541
- await client.workflows.run(groupId, triggerId, { payload: { ... } });
1542
- await client.workflows.runByWebhook('/orders', 'POST', { body: { ... } });
1543
-
1544
- const functions = await client.functions.list();
1545
- await client.functions.push(fn);
1546
- const fn = await client.functions.pull(functionId);
1547
- await client.functions.invoke(functionId, { args: { ... } });
1548
-
1549
- await client.projects.create({ name: 'My Project' });
1550
- const projects = await client.projects.list();
1551
- ```
1552
-
1553
- ### Run connector from online
1554
-
1555
- Execute connectors stored in your Cliodot account:
1556
-
1557
- ```javascript
1558
- const result = await flosync.runConnector('paystack', 'Initialize Transaction', {
1559
- body: { email: 'user@example.com', amount: 5000, reference: 'ref_123' },
1560
- });
1561
-
1562
- const { connectors, pagination } = await client.connectors.list();
1563
- const { installations } = await client.connectors.installed();
1564
- const connector = await client.connectors.get('paystack');
1565
- const data = await client.connectors.execute('paystack', 'Verify Transaction', {
1566
- pathParams: { reference: 'ref_123' },
1567
- });
1568
- ```
1569
-
1570
- | Method | Description |
1571
- |--------|-------------|
1572
- | `flosync.runConnector(id, action, options, { remote? })` | Run connector (local if registered, else remote) |
1573
- | `client.connectors.get(id)` | Get connector definition |
1574
- | `client.connectors.list(options?)` | List connectors (type, status, category, page, limit) |
1575
- | `client.connectors.search(options?)` | Search public connectors |
1576
- | `client.connectors.push(connector)` | Create or update connector (by `_id` or `slug`) |
1577
- | `client.connectors.installed(options?)` | List installed connectors |
1578
- | `client.connectors.install(id, { auth?, base_url? })` | Install connector |
1579
- | `client.connectors.uninstall(id)` | Uninstall connector |
1580
- | `client.connectors.execute(id, action, options)` | Execute connector action remotely |
1581
-
1582
- ### Workflows
1583
-
1584
- | Method | Description |
1585
- |--------|-------------|
1586
- | `client.workflows.push(workflow)` | Create or update workflow |
1587
- | `client.workflows.pull(groupId)` | Fetch workflow as IWorkflow |
1588
- | `client.workflows.list(options?)` | List workflow groups |
1589
- | `client.workflows.run(groupId, triggerId, payload)` | Run by trigger ID (payload may include `environment: "dev" \| "prod"`) |
1590
- | `client.workflows.runByWebhook(path, method, payload)` | **`POST /workflows/test/webhook`** — run the workflow whose HTTP trigger matches `path` and `method` (no group id in the request). Payload may include `environment`, `executionHeaders`, and the child body fields (merged into `payload` for the API). |
1591
- | `client.workflows.promote(groupId)` | Promote to production |
1592
-
1593
- See [Workflows](../docs/WORKFLOWS.md) for full documentation. See [Building and Pushing Custom Connectors](../docs/BUILDING_CONNECTORS.md) for connectors.
1594
-
1595
- ## Custom Connectors
1596
-
1597
- See [Building and Pushing Custom Connectors](../docs/BUILDING_CONNECTORS.md) for full samples of JSON, ConnectorBuilder, and custom execute connectors.
1598
-
1599
- ### REST connectors
1600
-
1601
- ```javascript
1602
- flosync.registerConnector('my-connector', {
1603
- _id: 'my-connector',
1604
- type: 'REST',
1605
- base_url: 'https://api.example.com',
1606
- auth: { type: 'bearer', token: process.env.MY_API_KEY },
1607
- endpoints: [
1608
- { name: 'GetUser', method: 'GET', path: '/users/:id' },
1609
- ],
1610
- });
1611
- ```
1612
-
1613
- ### Custom execute connector
1614
-
1615
- For full control, provide an `execute` function. The engine calls it with `(action, options, context)`.
1616
-
1617
- **Connector definition**
1618
-
1619
- ```javascript
1620
- flosync.registerConnector('my.auth', {
1621
- _id: 'my.auth',
1622
- type: 'system',
1623
- name: 'My Auth',
1624
- meta: { category: 'authentication' },
1625
- auth: { type: 'none' },
1626
- execute: async (action, options, context) => { ... },
1627
- });
1628
- ```
1629
-
1630
- Import types for TypeScript: `ConnectorExecuteOptions`, `ConnectorContext`, `ConnectorExecuteFn` from `cliodot`.
1631
-
1632
- **Execute signature**
1633
-
1634
- ```typescript
1635
- execute(
1636
- action: string,
1637
- options: ConnectorExecuteOptions,
1638
- context: ConnectorContext
1639
- ): Promise<unknown>
1640
- ```
1641
-
1642
- **Options** (from workflow step config):
1643
-
1644
- | Field | When used | Description |
1645
- |-------|-----------|-------------|
1646
- | `body` | API, auth, db, encrypt | Request body or payload |
1647
- | `params` | API | Query params |
1648
- | `pathParams` | API | Path params (e.g. `:id` in `/users/:id`) |
1649
- | `headers` | API | Extra headers |
1650
- | `database` | DB | Database name override |
1651
- | `connection_id` | DB | Connection ID override |
1652
-
1653
- **Context** (from run state):
1654
-
1655
- | Field | Description |
1656
- |-------|-------------|
1657
- | `headers` | HTTP headers from the trigger |
1658
- | `connectorConfig` | Per-connector config from `configure({ connectors: { 'id': {...} } })` |
1659
- | `env` | Environment overrides |
1660
- | `trigger` | Trigger payload (body, pathParams, params, etc.) |
1661
- | `stepResults` | Results of previous steps |
1662
- | `vars` | Workflow variables |
1663
-
1664
- **Discovering IDs and actions**
1665
-
1666
- - Built-in: `ConnectorId`, `ConnectorActions`, `getConnectorActions(connectorId)`, `listBuiltInConnectors()`
1667
- - Custom: `defineCustomConnector(id, actions)` or `defineCustomConnectorFromList(id, ['action1', 'action2'])`
1668
-
1669
- **REST endpoint shape**
1670
-
1671
- ```typescript
1672
- {
1673
- name: string; // Action name (matches s.connector(id, name, config))
1674
- action?: string; // Alias for name
1675
- method: string; // GET, POST, PUT, DELETE, etc.
1676
- path: string; // e.g. '/users/:id' or '/orders'
1677
- headers?: Record<string, string>;
1678
- body_schema?: object;
1679
- response_mapping?: Record<string, string>; // { "userId": "data.id" }
1680
- timeout_ms?: number;
1681
- }
1682
- ```
69
+ ## Documentation
1683
70
 
1684
- **Return format**
71
+ All guides, API references, and examples live on **[docs.cliodot.com](https://docs.cliodot.com)**:
1685
72
 
1686
- - API/DB/Encrypt: `{ raw, mapped }` or any object (engine uses `mapped ?? raw`)
1687
- - Auth: `{ valid: boolean, token?, user?, error? }` for success/failure branching
73
+ | Topic | Docs |
74
+ |-------|------|
75
+ | Getting started | [docs.cliodot.com/getting-started](https://docs.cliodot.com/getting-started) |
76
+ | SDK overview | [docs.cliodot.com/sdk](https://docs.cliodot.com/sdk) |
77
+ | Workflows | [docs.cliodot.com/workflows](https://docs.cliodot.com/workflows) |
78
+ | Functions | [docs.cliodot.com/functions](https://docs.cliodot.com/functions) |
79
+ | Connectors | [docs.cliodot.com/connectors](https://docs.cliodot.com/connectors) |
80
+ | API client | [docs.cliodot.com/api-client](https://docs.cliodot.com/api-client) |
81
+ | OAuth apps | [docs.cliodot.com/oauth-apps](https://docs.cliodot.com/oauth-apps) |
82
+ | Auth apps (MFA) | [docs.cliodot.com/auth-apps](https://docs.cliodot.com/auth-apps) |
1688
83
 
1689
84
  ## License
1690
85