Imported from SaucecodeOfficial/roboteur-skills (
plugins/roboteur-legacy/skills/roboteur-legacy/SKILL.md). Install upstream withnpx skills add SaucecodeOfficial/roboteur-skills --skill roboteur-legacy. Copyright stays with the author.
Legacy Roboteur Integration Guide
Help the user integrate with the legacy Roboteur platform (Feathers.js v4, z1 framework). This skill provides patterns for Record Store CRUD, Job Studio triggering, and real-time subscriptions.
Platform Overview
Roboteur is a hyper-automation + RPA platform built on:
- Server: Feathers.js v4 (REST + Socket.io)
- Auth: JWT via
authenticationservice - Database: PostgreSQL via Knex
- Real-time: Socket.io channels with event subscriptions
- Key domain: Shapes (schemas) → Record Stores (data) → Jobs (automation) → Bots (execution)
1. Feathers Client Setup
Browser Client (Socket.io — real-time + REST)
import feathers from '@feathersjs/feathers'
import socketio from '@feathersjs/socketio-client'
import auth from '@feathersjs/authentication-client'
import io from 'socket.io-client'
const socket = io('https://your-roboteur-server.com', {
transports: ['websocket'],
})
const client = feathers()
client.configure(socketio(socket))
client.configure(auth({ storage: window.localStorage }))
export default client
Node.js Client (REST only)
import feathers from '@feathersjs/feathers'
import rest from '@feathersjs/rest-client'
import auth from '@feathersjs/authentication-client'
import fetch from 'node-fetch'
const client = feathers()
const restClient = rest('https://your-roboteur-server.com')
client.configure(restClient.fetch(fetch, {
headers: { 'user-agent': 'node-xmlhttprequest' },
}))
client.configure(auth({}))
export default client
Authentication
// Login with credentials
const { accessToken, user } = await client.authenticate({
strategy: 'local',
email: 'user@example.com',
password: 'password',
})
// Re-authenticate with stored JWT
const result = await client.authentication.reAuthenticate()
// Logout
await client.logout()
Direct REST (no Feathers client)
# Get JWT token
POST /api/authentication
Content-Type: application/json
{ "strategy": "local", "email": "...", "password": "..." }
# Use token in subsequent requests
Authorization: Bearer <accessToken>
2. Record Store Integration
Core Services
| Service | Purpose | Response Format |
|---|---|---|
shape-storage |
Store metadata (name, shape, settings) | Paginated { data, total, limit, skip } |
shape-storage-record |
Record CRUD within a paginated store | Paginated { data, total, limit, skip } |
static-record-store |
Record CRUD within a static (in-memory SQLite) store | Raw array [] — NO pagination wrapper |
Two Store Types
Paginated stores (shape-storage-record) — PostgreSQL-backed, standard Feathers pagination format.
Static stores (static-record-store) — In-memory SQLite-backed queues/working sets. These return raw arrays, not { data, total }. They use a composite ID format storageId__recordId for get/patch/remove, and support a custom fieldOperations query for JSON path filtering.
Record Structure
interface RecordStoreRecord {
_id: string // UUID v4
storageId: string // shape-storage ID (links record to store)
entryData: Record<string, any> // User-defined fields (shape-driven)
linkedData?: Record<string, any> // Resolved linked fields
createdAt: string // ISO date
updatedAt: string // ISO date
createdBy?: string // User ID
updatedBy?: string // User ID
}
Find Stores
// List all stores (paginated)
const stores = await client.service('shape-storage').find({
query: {
$limit: 1000,
$sort: { createdAt: -1 },
},
})
// stores.data = ShapeStorage[], stores.total = number
// Find store by name
const result = await client.service('shape-storage').find({
query: {
name: 'User Accounts',
$limit: 1,
},
})
const store = result.data[0]
Record CRUD
const recordService = client.service('shape-storage-record')
// LIST records in a store
const records = await recordService.find({
query: {
storageId: '<store-id>',
$limit: 1000,
$sort: { createdAt: -1 },
},
})
// GET single record
const record = await recordService.get('<record-id>')
// CREATE record
const newRecord = await recordService.create({
storageId: '<store-id>',
entryData: {
firstName: 'John',
lastName: 'Doe',
email: 'john@example.com',
},
})
// UPDATE record (patch = partial update)
const updated = await recordService.patch('<record-id>', {
entryData: {
email: 'john.doe@example.com',
},
})
// REMOVE record
await recordService.remove('<record-id>')
REST API Equivalent
GET /api/shape-storage-record?storageId=<id>&$limit=1000&$sort[createdAt]=-1
GET /api/shape-storage-record/<record-id>
POST /api/shape-storage-record { storageId, entryData: {...} }
PATCH /api/shape-storage-record/<record-id> { entryData: {...} }
DELETE /api/shape-storage-record/<record-id>
Query Operators
// Pagination
{ $limit: 100, $skip: 200 }
// Sorting (1 = ascending, -1 = descending)
{ $sort: { createdAt: -1 } }
// Select specific fields
{ $select: ['_id', 'entryData', 'createdAt'] }
// Equality filter
{ storageId: '<id>' }
// Not equal
{ _id: { $ne: '<exclude-id>' } }
// In array
{ status: { $in: ['active', 'pending'] } }
// Greater/less than
{ createdAt: { $gt: '2024-01-01' } }
// Combine filters
{
storageId: '<id>',
'entryData.status': 'active',
$limit: 50,
$sort: { updatedAt: -1 },
}
Static Record Store (static-record-store)
Static stores use in-memory SQLite (via storeEngine) for high-performance queue/working-set operations. Key differences from paginated stores:
- Returns raw arrays —
findreturnsRecord[], NOT{ data, total } - Composite ID —
get,patch,removeusestorageId__recordIdformat - No
$skip— no pagination offset, use$limitand$sortonly fieldOperations— custom JSON path query system for filteringentryDatafields
CRUD Operations
const staticService = client.service('static-record-store')
// FIND — returns Record[] (raw array, no pagination wrapper!)
const records = await staticService.find({
query: {
storageId: '<store-id>',
$sort: { createdAt: -1 },
$limit: 100,
},
})
// records = [ { _id, storageId, entryData, createdAt, updatedAt }, ... ]
// GET — uses composite ID: storageId__recordId
const record = await staticService.get('<store-id>__<record-id>')
// CREATE
const newRecord = await staticService.create({
storageId: '<store-id>',
entryData: { status: 'pending', accountNumber: '12345' },
})
// PATCH — composite ID
const updated = await staticService.patch('<store-id>__<record-id>', {
entryData: { status: 'complete' },
})
// REMOVE — composite ID
const removed = await staticService.remove('<store-id>__<record-id>')
Field Operations Query
The fieldOperations parameter enables structured filtering on entryData JSON fields. Pass as an array of operation objects (or a JSON string):
interface FieldOperation {
field: string // Field name in entryData (or '_id' for record ID)
operation: string // Operator name (see table below)
predicate: any // Value to compare against (array for 'match'/'notMatch')
}
Available operators:
| Operation | SQL Operator | Description |
|---|---|---|
| (default) | = |
Equals |
notEqual |
!= |
Not equals |
contains |
LIKE |
Contains text (auto-wraps with %) |
notContains |
NOT LIKE |
Does not contain text (auto-wraps with %) |
match |
IN |
Matches any value in predicate array |
notMatch |
NOT IN |
Does not match any value in predicate array |
greaterThan |
> |
Greater than |
greaterThanEqual |
>= |
Greater than or equal |
lessThan |
< |
Less than |
lessThanEqual |
<= |
Less than or equal |
Examples:
// Filter by a single field
const active = await staticService.find({
query: {
storageId: '<store-id>',
fieldOperations: [
{ field: 'status', operation: 'equal', predicate: 'active' },
],
},
})
// Contains text search
const matching = await staticService.find({
query: {
storageId: '<store-id>',
fieldOperations: [
{ field: 'accountNumber', operation: 'contains', predicate: '6265' },
],
},
})
// Match multiple values (IN query)
const selected = await staticService.find({
query: {
storageId: '<store-id>',
fieldOperations: [
{
field: 'status',
operation: 'match',
predicate: ['pending', 'processing', 'retry'],
},
],
},
})
// Multiple field operations (AND — first op is WHERE, rest are AND WHERE)
const filtered = await staticService.find({
query: {
storageId: '<store-id>',
fieldOperations: [
{ field: 'status', operation: 'equal', predicate: 'complete' },
{ field: 'amount', operation: 'greaterThan', predicate: 1000 },
],
$sort: { updatedAt: -1 },
$limit: 50,
},
})
// Filter by _id directly (bypasses JSON path, queries _id column)
const byIds = await staticService.find({
query: {
storageId: '<store-id>',
fieldOperations: [
{
field: '_id',
operation: 'match',
predicate: ['uuid-1', 'uuid-2', 'uuid-3'],
},
],
},
})
// Pass fieldOperations as JSON string (useful in REST URLs)
// GET /api/static-record-store?storageId=xxx&fieldOperations=[{"field":"status","operation":"equal","predicate":"active"}]
How it works internally:
- The first
fieldOperationbecomes the initialWHEREclause - Subsequent operations are chained as
AND WHERE - For
entryDatafields, it uses SQLite'swhereJsonPath('entryData', '$.fieldName', operator, value) - For
_idfield, it queries the_idcolumn directly - Field names are auto-converted to camelCase
contains/notContainsoperations auto-wrap the predicate with%forLIKEqueries
3. Job Studio & Service Integration
Core Services
| Service | Purpose |
|---|---|
job-service |
Job/service definitions and settings |
job-service-reaction |
Execution instances (reactions) |
job-service-reaction-run |
Individual run tracking |
job-service-reaction-run-stream |
Run output/alert stream |
Trigger a Service Reaction via API
The API Endpoint reactor allows external systems to trigger job executions:
// 1. Find the service
const services = await client.service('job-service').find({
query: { name: 'My Service', $limit: 1 },
})
const service = services.data[0]
// 2. Trigger the reaction
await client.service('job-service-reaction').create({
serviceId: service._id,
reactorName: 'API Endpoint Reactor Name', // matches the reactor's Reaction Name
payload: {
// Record data matching the reactor's shape
accountNumber: '12345',
amount: 500,
},
})
REST Trigger
POST /api/job-service-reaction
Authorization: Bearer <token>
Content-Type: application/json
{
"serviceId": "<service-id>",
"reactorName": "<reaction-name>",
"payload": { ... }
}
Monitor Reactions
// List reactions for a service
const reactions = await client.service('job-service-reaction').find({
query: {
serviceId: '<service-id>',
$sort: { createdAt: -1 },
$limit: 50,
},
})
// Reaction states: running, complete, failed, stopped
Job Studio Record Store Commands (Context)
When building automations in Job Studio, these commands interact with Record Stores:
| Command | Purpose |
|---|---|
getStoreRecordList |
Get all records from a store by Store Key (storageId) |
setStoreRecord |
Set/overwrite a record in a store |
appendRecordToRecordList |
Append a record to a store's record list |
findRecordInStoreRecordList |
Find a specific record in a store |
selectRecordInStoreRecordList |
Select a record from a store list |
updateRecordInStoreRecordList |
Update a record in a store list |
queryStoreRecordList |
Query records with filters |
removeByQuery |
Remove records matching a query |
mergeRecordList |
Merge records into a store |
truncateStoreRecordList |
Clear all records from a store |
updateRecordsByQuery |
Batch update records matching criteria |
Job Studio Reactors (Triggers)
| Reactor | Trigger |
|---|---|
apiEndpoint |
External REST POST request |
pushButton |
Manual button press in UI |
manualInput |
Manual form input in UI |
schedule |
Cron-based time schedule |
recordStoreChange |
React to record store create/update/remove events |
selectRecords |
React to user selecting records in a store |
cloudFile |
React to cloud file upload |
reactionComplete |
Chain — react when another reaction completes |
4. Real-Time Subscriptions (Socket.io)
// Subscribe to record store changes
const recordService = client.service('shape-storage-record')
recordService.on('created', (record) => {
console.log('New record:', record)
})
recordService.on('patched', (record) => {
console.log('Updated record:', record)
})
recordService.on('removed', (record) => {
console.log('Removed record:', record)
})
// Subscribe to service reaction updates
client.service('job-service-reaction').on('patched', (reaction) => {
console.log('Reaction status:', reaction.status)
})
// Subscribe to reaction run stream (logs/alerts)
client.service('job-service-reaction-run-stream').on('created', (entry) => {
console.log('Run stream:', entry)
})
// Unsubscribe
recordService.removeAllListeners('created')
Connection Monitoring
// Socket.io connection events
client.io.on('connect', () => console.log('Connected'))
client.io.on('disconnect', () => console.log('Disconnected'))
client.io.on('reconnect', () => console.log('Reconnected'))
5. Service Timeouts
Long-running operations may need extended timeouts:
// Set timeout per service call (milliseconds)
client.service('shape-storage-record').timeout = 60000 // 60s
client.service('authentication').timeout = 30000 // 30s
6. Error Handling
Feathers returns structured errors:
try {
await client.service('shape-storage-record').create({ ... })
} catch (error) {
// error.code — HTTP status (400, 401, 404, 409, 500)
// error.message — Human-readable message
// error.className — Feathers error class (bad-request, not-authenticated, etc.)
// error.data — Additional error context
if (error.code === 401) {
// Re-authenticate
await client.authentication.reAuthenticate()
}
}
Common Error Codes
| Code | Class | Meaning |
|---|---|---|
| 400 | bad-request |
Invalid data or duplicate entry |
| 401 | not-authenticated |
Missing or expired JWT |
| 403 | forbidden |
Insufficient permissions |
| 404 | not-found |
Record/service not found |
| 408 | timeout |
Request timed out |
| 409 | conflict |
Conflict (e.g. duplicate slug) |
7. Common Integration Patterns
Dashboard: Polling Record Store
// Poll a record store every 30 seconds
async function pollRecords(storageId: string) {
const records = await client.service('shape-storage-record').find({
query: {
storageId,
$sort: { updatedAt: -1 },
$limit: 100,
},
})
return records.data
}
Pipeline: Trigger Job with Record Data
// 1. Get records from source store
const source = await client.service('shape-storage-record').find({
query: { storageId: '<source-store-id>', $limit: 10000 },
})
// 2. Process and write to target store
for (const record of source.data) {
await client.service('shape-storage-record').create({
storageId: '<target-store-id>',
entryData: transformRecord(record.entryData),
})
}
// 3. Trigger downstream job
await client.service('job-service-reaction').create({
serviceId: '<service-id>',
reactorName: 'Process Complete',
payload: { count: source.data.length },
})
Bulk Operations
// Feathers supports array create
const records = items.map(item => ({
storageId: '<store-id>',
entryData: item,
}))
const created = await client.service('shape-storage-record').create(records)
8. Shape System (Schema Reference)
Shapes define the structure of record store fields:
interface Shape {
_id: string
name: string
namespace: string
slug: string // '@namespace_name' format
version: string // nanoid — immutable versions
schema: {
schema: JSONSchema // JSON Schema for validation
uiSchema: UISchema // react-jsonschema-form UI hints
}
tree: ShapeField[] // Ordered field definitions
fieldCount: number
}
// Data types available in shapes:
// boolean, text, long-text, number, number-list, text-list,
// password, data-field, data-item, data-list, key-list,
// position, size, date, time, select, multi-select,
// file, image, color, json
8.1 Authoring importable Shape JSON files
Shape Studio supports Import from JSON. A valid import file has a strict envelope and per-tree-item schema. Missing any of the required envelope fields triggers the "validate package" step to fail silently (no useful error, just refuses to import).
A minimal import-verified template is at §8.1.6 below, and a complete shape appears as the shape dependency inside the bundled worked example (data/examples/pkg_job_ACME-Invoice-Demo.json) — compare against those whenever authoring a new shape.
8.1.1 Top-level envelope — required fields
| Field | Type | Notes |
|---|---|---|
_id |
UUID v4 | Required — import validation fails without it. Must be a fresh UUID per shape. Does not collide with server-generated records; the server accepts this as the authoritative identity. |
name |
string | Display name |
namespace |
string | e.g. "acme" — groups shapes |
slug |
string | @namespace_name format — e.g. "@acme_pending-invoices" |
entity |
"shape" |
Literal — required |
description |
string | Free text; "dev\n" is a harmless default |
settings |
{title, description} |
Display config; mirror the top-level name/description |
version |
nanoid | 21 chars from [A-Za-z0-9_-]. Immutable — any change to the shape increments this |
packageVersion |
string | e.g. "0.1.0" |
packagePublishDate |
null | ISO date |
null is fine for unpublished shapes |
packagePublishId |
null | string |
null is fine |
fieldCount |
integer | Must equal tree.length — import validation checks this |
dependencies |
[] |
Usually empty |
importedFrom |
null |
null for hand-authored shapes |
tree |
array | Field definitions — see 8.1.2 |
schema |
{schema, uiSchema} |
JSON Schema + rjsf UI hints — see 8.1.3 |
Do NOT include (server generates on import):
createdAt,updatedAt,createdBy,updatedByat the top level
8.1.2 Tree items — required fields per field
Each entry in tree describes one field. Required keys:
| Field | Type | Notes |
|---|---|---|
version |
"7.5.1" |
Field-type-definition version. The reference shapes use this value; stick with it unless the server rejects |
fieldType |
canonical string | See 8.1.4 — must match a known field type or the field renders as empty |
title |
string | Display label |
dataType |
canonical string | text, long-text, boolean, text-list, json, number, date, etc. |
fieldGroup |
string | "text" for most, "list" for array types |
icon |
FontAwesome name | See 8.1.4 table |
color |
Tailwind class name | e.g. "blue-500" — NOT hex codes |
accepts |
[] |
Reserved for linked fields; empty array for basic fields |
meta |
null |
Reserved; null for basic fields |
id |
nanoid | 21-char nanoid, unique per tree item |
name |
camelCase string | The JSON property name in entryData |
options |
array | Only for segment/textSelect/segmentMulti/textSelectMulti — [{value, title}, ...] |
Do NOT include (server generates or references, omit safely):
_idon tree items — in production exports this references a shared field-type-definition UUID, but can be omitted on importcreatedAt,updatedAton tree items
8.1.3 schema block — JSON Schema + uiSchema
Keep it flat — mirror the reference template's shape rather than deeply nested JSON Schema. Shape Studio does not validate nested object schemas deeply.
{
"schema": {
"schema": {
"type": "object",
"title": "My Shape",
"description": "dev\n",
"required": [],
"properties": {
"fieldName": { "title": "Field Label", "type": "string", "children": [] }
}
},
"uiSchema": {
"classNames": "shape-fields",
"fieldName": { "ui:placeholder": "Enter a value", "ui:disabled": false }
}
}
}
Per-type uiSchema patterns:
| fieldType | uiSchema entry |
|---|---|
textGeneral / email / date |
{ "ui:placeholder": "Enter a value", "ui:disabled": false } |
textarea |
{ "ui:placeholder": "...", "ui:disabled": false, "ui:widget": "textarea" } |
json |
{ "ui:widget": "JsonTextArea" } |
segment |
{ "ui:widget": "SegmentSelect", "ui:options": { "x": "left", "size": "xs", "borderWidth": true, "buttons": { "<value>": { "label": "<title>", "icon": "check-circle" } } } } |
toggle |
{ "ui:placeholder": "Enter a value", "ui:disabled": false } |
8.1.4 Canonical fieldType taxonomy
This table is the canonical taxonomy, transcribed from the Shape Studio field-type definitions.
Common mistake: using intuitive names like text, select, boolean, textArea — none of these are valid fieldType values. Use the canonical names below.
| Purpose | fieldType |
dataType |
icon |
color |
fieldGroup |
|---|---|---|---|---|---|
| Basic text input | textGeneral |
text |
spell-check |
blue-500 |
text |
| Single-select buttons | segment |
text |
grip-horizontal |
blue-500 |
text |
| Single-select list | textSelect |
text |
list-alt |
blue-500 |
text |
| Multi-line text | textarea |
long-text |
file-alt |
blue-500 |
text |
| JSON blob | json |
json |
code |
blue-500 |
text |
| Boolean toggle | toggle |
boolean |
check-square |
blue-500 |
text |
| Email (validated) | email |
text |
envelope |
blue-500 |
text |
| URL (validated) | url |
text |
link |
blue-500 |
text |
| Date picker | date |
text (or date) |
calendar |
blue-500 |
text |
| DateTime picker | dateTime |
text |
clock |
blue-500 |
text |
| Password | password |
text |
key |
blue-500 |
text |
| Number | numberGeneral |
number |
hashtag |
blue-500 |
text |
| Integer | integer |
number |
hashtag |
blue-500 |
text |
| Decimal | decimal |
number |
hashtag |
blue-500 |
text |
| Array of text | textList |
text-list |
list |
blue-500 |
list |
| Array of numbers | numberList |
number-list |
list-ol |
blue-500 |
list |
| Multi-select buttons | segmentMulti |
text-list |
grip-horizontal |
blue-500 |
list |
| Multi-select items | textSelectMulti |
text-list |
list-alt |
blue-500 |
list |
| Structured item (repeatable) | dataItem |
— | database |
blue-500 |
data |
| Structured list (repeatable) | dataList |
— | database |
blue-500 |
data |
Critical distinction: use json (not dataItem) for a free-form JSON blob. dataItem is a structured repeatable-item container in legacy — it forces the author to define child sub-fields. json uses the simple JsonTextArea widget with default: {}.
8.1.5 nanoid generation
Generate nanoids for top-level version and each tree-item id. 21 chars, alphabet A-Za-z0-9_-:
node -e "
const crypto = require('crypto');
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-';
function nano() {
const buf = crypto.randomBytes(21);
let out = '';
for (let i = 0; i < 21; i++) out += alphabet[buf[i] % 64];
return out;
}
function uuid() {
const b = crypto.randomBytes(16);
b[6] = (b[6] & 0x0f) | 0x40;
b[8] = (b[8] & 0x3f) | 0x80;
const h = b.toString('hex');
return h.substr(0,8)+'-'+h.substr(8,4)+'-'+h.substr(12,4)+'-'+h.substr(16,4)+'-'+h.substr(20,12);
}
console.log('shape _id: ', uuid());
console.log('shape version:', nano());
for (let i = 0; i < 10; i++) console.log('tree id '+i+':', nano());
"
8.1.6 Minimal working template
Drop-in starter. Fill in _id, version, and each tree-item id with fresh values, then edit the single status field or add more. This template has been verified to import cleanly:
{
"_id": "<fresh UUID v4>",
"name": "My New Shape",
"namespace": "myns",
"slug": "@myns_my-new-shape",
"entity": "shape",
"description": "dev\n",
"settings": { "title": "My New Shape", "description": "dev\n" },
"version": "<fresh nanoid-21>",
"packageVersion": "0.1.0",
"packagePublishDate": null,
"packagePublishId": null,
"fieldCount": 1,
"dependencies": [],
"importedFrom": null,
"tree": [
{
"version": "7.5.1",
"fieldType": "segment",
"title": "Status",
"dataType": "text",
"fieldGroup": "text",
"icon": "grip-horizontal",
"color": "blue-500",
"accepts": [],
"meta": null,
"id": "<fresh nanoid-21>",
"name": "status",
"options": [
{ "value": "waiting", "title": "Waiting" },
{ "value": "complete", "title": "Complete" }
]
}
],
"schema": {
"schema": {
"type": "object",
"title": "My New Shape",
"description": "dev\n",
"required": [],
"properties": {
"status": { "title": "Status", "type": "string" }
}
},
"uiSchema": {
"classNames": "shape-fields",
"status": {
"ui:widget": "SegmentSelect",
"ui:options": {
"x": "left",
"size": "xs",
"borderWidth": true,
"buttons": {
"waiting": { "label": "Waiting", "icon": "check-circle" },
"complete": { "label": "Complete", "icon": "check-circle" }
}
}
}
}
}
}
8.1.7 Gotchas checklist — run through before submitting
- Top-level
_idis a fresh UUID (not omitted, not reused) - Top-level
versionis a 21-char nanoid (not a semver string) -
fieldCountmatchestree.lengthexactly -
entity: "shape"is present - Every tree item has a unique
id(21-char nanoid) — duplicates silently break the shape editor - Every tree item has
accepts: []andmeta: null - No tree item has
_id,createdAt, orupdatedAt(let the server handle those) -
fieldTypevalues are the canonical ones from 8.1.4 — nottext/select/boolean/textArea - JSON blob fields use
fieldType: "json"withdataType: "json"(notdataItem) - Select fields have
options: [{value, title}, ...] -
schema.schema.propertieshas an entry for every tree-itemname -
schema.uiSchemahas an entry for every tree-itemname(at minimum{"ui:placeholder": "...", "ui:disabled": false})
8.1.8 Reference implementations
The template in §8.1.6 is import-verified as-is. For a shape embedded in a full
export package (with segment options, a json blob, and the dependency
envelope), see the shape entry in the bundled
data/examples/pkg_job_ACME-Invoice-Demo.json.
9. Interpreting Exported Job / Micro / Macro Flows
This section documents the export package format for jobs, micros, and macros, and how to statically interpret one — extracting every node, its data, and the execution order — so a flow can be migrated to a maintainable CLI without running the legacy engine.
Goal context: these exports are the source of truth for "what a legacy automation actually does." The legacy Job/Micro/Macro studios render the same
schema.flowgraph this section describes. To migrate a flow, you reconstruct its node list + data bindings + execution order from the static JSON — you do not need a running Roboteur server.
Reference material bundled with this skill:
- Command toolboxes (the catalog of every available node type):
data/toolboxes/{job,micro,macro}-commands.json - Worked export example (a complete job package with micro/macro/shape/record dependencies):
data/examples/pkg_job_ACME-Invoice-Demo.json
9.1 Three execution engines, three toolboxes
A flow is built from commands (a.k.a. nodes). There are three command catalogs, one per engine. Each node in an export carries command + method which join to exactly one toolbox entry (also keyed by command/method, plus a flat key).
| Toolbox file | Engine | Format | Count | Runs where (roles) |
Command groups |
|---|---|---|---|---|---|
job-commands.json |
Job — server orchestration | CSV (note .json ext is a misnomer) |
161 | server |
control, logic, reactor, rest, store, system |
micro-commands.json |
Micro — reusable server sub-flow | CSV | 147 | server |
control, logic, … (a subset of job + micro_input/micro_output/set_state) |
macro-commands.json |
Macro — desktop/robot automation | JSON array | 267 | robot, server |
apps, clipboard, diagram, fs, image, keyboard, mouse, window, playbrowser, system, robot, logic, rest, control, combos |
- Job nodes orchestrate: trigger reactors, run macros/micros, transform data, hit REST/SQL/Kafka/mail, and read/write record stores (
storegroup → the Job Studio record-store commands in §3). - Micro nodes are the same server primitives, packaged as a reusable sub-flow with a typed input/output interface (
micro_input/micro_outputportals). - Macro nodes are the desktop bot vocabulary: keyboard/mouse, window management, Excel/PDF (
apps), filesystem (fs), image ops, headless browser (playbrowser),powershell/execute.roles: ["robot","server"]means they execute on a robot machine.
Toolbox row schema (both CSV and JSON share these columns/keys):
_id, createdAt, updatedAt, command, method, name, settings, shouldSync, canFork, dynamic, frozen, icon, color, type, key, description, roles, version, inputs, outputs, docUrl
inputs/outputsdefine the port contract for that command (the ports every instance starts with).- CSV caveat: in
job-commands.json/micro-commands.json, theinputs/outputscells are loose JSON with unquoted keys (e.g.[{direction:in,name:>>_run,dataType:trigger,...}]) —JSON.parsewill reject them. Parse the CSV first (handle quoted fields), then relax the cell (quote bare keys) or hand-tokenize.macro-commands.jsonis a normal JSON array and parses directly.
9.2 The package export envelope
Every export is one JSON object with entity ∈ {"job","micro","macro"}. Top-level keys:
| Field | Meaning |
|---|---|
_id, name, namespace, slug, version |
Identity. slug = @namespace_name; version is a nanoid (immutable per edit). (slug, version) is the join key dependencies are referenced by. |
packageVersion, packagePublishDate, packagePublishId, importedFrom |
Packaging metadata (usually 0.1.0 / null). |
commandCount |
Number of custom-node elements in schema.flow (verify your parse against this). |
macroCount, microCount, shapeCount, reactorCount, robotCount |
Composition counts. |
dependencies[] |
Flattened list of every referenced micro, macro, shape, and record (see §9.8). |
schema |
{ enter, flow, internal, cache, state, exit } — the execution graph (§9.3). |
flow |
{ position, zoom } — canvas viewport only; ignore for migration. |
settings[] |
Job-level service settings/inputs (typed defaults: defaultText, defaultNumber, defaultDataList, …). For micros/macros the typed interface lives in inputs/outputs/internal instead. |
replayState[] |
Saved sample/test record data used by the studio's replay/debug mode — useful as fixtures when validating a migrated flow. |
swarm[] |
Robot assignments (empty for server-only jobs). |
entity |
"job" | "micro" | "macro". |
9.3 The execution graph — schema
schema has six keys; the graph is in flow:
| Key | Role |
|---|---|
enter |
The entry node. Job: command:control / method:replayStart, key:job_in. Micro/Macro: method:inputPortal. Its out[] ports' targets[] are the first links fired. |
flow |
Array mixing nodes and edges, discriminated by type: "custom-node" = node (count == commandCount), "pro" = edge (ReactFlow link). |
exit |
The terminal node. Job: key:job_out. Micro/Macro: method:outputPortal. |
internal, cache, state |
Runtime scratch (internal portal values, memoized results, per-run state). Usually {} at export; not needed to reconstruct logic. |
9.4 Nodes (type: "custom-node")
| Field | Meaning |
|---|---|
nodeId |
Unique 21-char id. The vertex identity used by all edges/links. |
command, method, key |
Joins to the toolbox entry (§9.1) → tells you what the node does. |
name, alias |
Display name; alias is the author's per-instance label (e.g. "Validate Invoice Totals") — capture it, it's the human intent. |
roles |
["server"] or ["robot","server"] — where it runs. |
in[] |
Input ports — the node's parameters (§9.5). |
out[] |
Output ports — the node's results (§9.5). |
next |
Primary control-flow continuation — { portId, targets: [{linkId, nodeId, portId, triggerCommand, triggerMethod, type:"flow"}] }. Mirror of the <<_continue out-port's targets. |
error |
Error-branch continuation — same shape, mirror of the <<_error out-port. |
position, status, toolbar, streaming, animated |
Editor/runtime state — ignore for migration (except position if you want to preserve layout). |
9.5 Ports — parameters and results
Port key convention: >>_xxx = input, <<_xxx = output. portId format is <randomId>>>{in|out}>>{dataType} — this exact string is what edges reference in sourceHandle/targetHandle.
Input port (in[]):
| Field | Meaning |
|---|---|
key, name, label, dataType |
Identity + type (text, long-text, number, boolean, data-item, data-list, record, record-list, trigger, password, …). |
data |
The literal value when supplied manually. null when the value arrives via a link. |
source |
"manual" → use data; "link" → value comes from another node's output. |
sourceType |
"manual-only", "link-manual" (either), etc. — the allowed binding modes. source is the actual one. |
sources[] |
When linked: [{ linkId, nodeId, portId, dataType, type:"flow" }] pointing back to the source node + out-port that feeds this input. |
required, accepts[] |
Validation: required flag; list of dataTypes this port will accept. |
Output port (out[]): same identity fields plus targets[] — [{ linkId, triggeredBy, triggerCommand, triggerMethod, nodeId, portId, type:"flow" }] listing every downstream port it feeds. Trigger out-ports (<<_continue, <<_error, dataType:trigger) drive control flow; typed out-ports feed data.
So a node's parameters are fully recoverable: for each in port, if source:"manual" take data; if source:"link" resolve sources[0] → {nodeId, portId} and read that upstream node's output.
9.6 Edges (type: "pro")
Edges are the explicit link records (the visual wires). Each:
{ source, sourceHandle, target, targetHandle, type:"pro", id,
style:{stroke,strokeWidth}, animated,
label:{ linkId, type, sourceNode, sourcePort, sourcePortType, targetNode, targetPort, targetPortType, repeat } }
source/target= node ids;sourceHandle/targetHandle=portIds (match §9.5).label.typeis the link's dataType."trigger"= control flow (green stroke#00ff6f); anything else (text,record,record-list,data-list,number,long-text, …) = a data link (other stroke colors).- Edges are redundant with the
next/targets/sourcespointers on nodes — both encode the same graph. Use whichever is convenient; cross-check them to validate your parse (linkIdis shared across both representations).
9.7 Entry / exit
- Entry: walk from
schema.enter. Itsout[]ports carrydata(constants the flow starts with — e.g. store paths/ids, thereplayStartseed) andtargets[](the first nodes to fire). Job entry also exposes the trigger/reactor outputs. - Exit:
schema.exit(job_out/outputPortal) — for micros/macros, itsin[]ports map to the package's declaredoutputs[](the values handed back to the caller).
9.8 Dependency resolution
dependencies[] is fully flattened: a job lists every micro, macro, shape, and record used anywhere in the flow (nested micros/macros have their own dependencies: [] — the job hoists them all). Discriminate by entity:
entity |
What it is | Key sub-structure |
|---|---|---|
micro |
Reusable server sub-flow | Own schema.{enter,flow,…} + inputs[] / outputs[] / internal[] (typed interface, keyed >>_/<<_). |
macro |
Desktop/robot sub-flow | Own schema.{enter,flow,…} + inputs[] (outputs/internal often empty). Nodes are macro-toolbox (robot) commands. |
shape |
Record store schema | The shape envelope from §8 (tree, schema, fieldCount). |
record |
Record store data snapshot | Seed/static records the flow reads. |
Binding a runMicro / runMacro node to its target: the node identifies its callee by (>>_slug, >>_version) port data, not by id — >>_micro_id / >>_macro_id are frequently undefined. Resolve by matching those two values against dependencies[].slug + dependencies[].version. The runner node's other >>_ input ports map positionally to the target micro/macro's declared inputs[]; its <<_ outputs map to the target's outputs[].
runMicro node: >>_slug = "@ACME-Demo_Validate-Invoice-Totals"
>>_version = "R0NVISgiDya-rwZZT4TMc"
└── matches dependency: micro slug "@ACME-Demo_Validate-Invoice-Totals" v"R0NVISgiDya-rwZZT4TMc"
To migrate the whole flow, recurse: resolve each runMicro/runMacro, interpret the target's schema.flow the same way, and inline or emit it as a callable unit.
9.9 Extraction recipe (for CLI migration)
Given an export file, reconstruct node details + data + execution order:
- Load & split. Parse the envelope. From
schema.flow, splitnodes = [type==="custom-node"]andedges = [type==="pro"]. Assertnodes.length === commandCount. - Index. Build
nodesById = Map(nodeId → node). BuildportToNode = Map(portId → nodeId)from every node'sin[]/out[]. - Resolve each node's inputs. For every
inport:source==="manual"→ value =data;source==="link"→ value = output ofsources[0].nodeIdatsources[0].portId(a data dependency edge). Record{ nodeId, command, method, alias, inputs:{key→value|ref}, roles }. - Build the control-flow graph. Edges with
label.type==="trigger"(equivalently each node'snext/error.targets) are directed control edgessourceNode → targetNode. Seed fromschema.enter.out[].targets. - Order. Topologically sort the trigger graph from
entertoexit. Branches: a node's<<_continuevs<<_errorout-ports give the success/failure successors;logicswitch/when nodes (when_true,flow_switch_by_text,when_record_is, …) andrepeat_*nodes fan out to multiple trigger targets — model these as conditionals/loops, not straight-line steps. Cross-check against edges via sharedlinkId. - Resolve sub-flows. For each
runMicro/runMacro, join(>>_slug,>>_version)todependencies[], map runner ports → targetinputs[]/outputs[], and recurse from step 1 on the target'sschema. - Classify by engine. Use
roles+ toolbox group to route each node:serverlogic/store/rest/system → CLI service calls;robotmacro commands → desktop-automation steps (or flag as needing the bot runtime). The toolbox entry'sdescription/docUrldocuments the intended semantics of eachmethod. - Fixtures. Use
replayState[](jobs) and dependencyrecords as test inputs to validate the migrated CLI against the legacy behavior.
A small Node script that does steps 1–6 over data/examples/ is the natural first deliverable of the CLI migration — it turns any export into a normalized { nodes, order, dataLinks, subflows } IR.
9.10 Quick field-location cheat sheet
| You want… | Look at… |
|---|---|
| What a node does | node.command/method → toolbox entry in data/toolboxes/ |
| A node's literal parameters | node.in[].data where source==="manual" |
| Where a node's input comes from | node.in[].sources[0] → {nodeId, portId} |
| What a node feeds downstream | node.out[].targets[] |
| Execution order | schema.enter.out[].targets → follow node.next.targets[].nodeId (trigger edges) |
| Error handling | node.error.targets[] / <<_error out-port |
| Which micro/macro a runner calls | node.in[>>_slug].data + [>>_version].data → dependencies[] |
| A sub-flow's interface | dependency.inputs[] / outputs[] (keyed >>_/<<_) |
| Shapes/records the flow uses | dependencies[] where entity is shape/record |
9.11 Companion scripts
This skill ships two project-agnostic Node scripts (no dependencies) in scripts/. They are the executable form of §9.9 — use them directly, or read them as the reference implementation when generating bespoke extraction for a specific migration.
scripts/extract-flow.js — turns any export into the normalized IR { package, nodes, order, dataNodes, triggerLinks, dataLinks, enter, exit, subflows, dependencies, warnings }. It resolves manual vs linked inputs, derives execution order by walking the trigger graph from enter, separates sequential steps (order) from data-provider nodes (dataNodes — inputPortal/internalPortal/etc.), and recurses into every runMicro/runMacro (joining (slug,version) → dependencies[], mapping runner ports to the callee's inputs[]/outputs[]).
node scripts/extract-flow.js <export.json> # full IR as JSON
node scripts/extract-flow.js <export.json> --summary # readable: order + sub-flow tree + warnings
node scripts/extract-flow.js <export.json> --no-subflows # don't recurse
node scripts/extract-flow.js <export.json> --depth 3 # cap sub-flow recursion (default 5)
A clean run reports 0 warnings; warnings flag commandCount mismatches, unresolved sub-flows (a runMicro/runMacro whose target isn't in dependencies), or genuinely orphaned trigger nodes. Verified against the bundled data/examples/ package and against real-world production exports (jobs + their micros + macros, recursively).
scripts/relax-toolbox-csv.js — loads the three command toolboxes from §9.1 into one Map("command/method" → { engine, group, name, description, docUrl, roles, inputs, outputs }), handling both the CSV files (job/micro, with their unquoted loose-JSON inputs/outputs cells) and the macro JSON array. Use it to annotate each IR node with its documented semantics (§9.9 step 7).
node scripts/relax-toolbox-csv.js data/toolboxes # catalog as JSON
node scripts/relax-toolbox-csv.js data/toolboxes --list # one line per command
Both are require()-able as modules (extract, extractGraph / loadCatalog, parseCSV, relaxJSON) so a migration CLI can compose them: load the catalog once, extract each export's IR, then walk ir.order emitting one CLI step per node enriched with the catalog entry.
Note: the toolbox catalog keys on
command/method; a handful of keys (e.g.control/runMicro) exist in more than one toolbox, so the Map keeps the last loaded (macro > micro > job). For per-engine fidelity, load each toolbox separately rather than the merged catalog.
Quick Reference: All Feathers Services
| Service | Methods | Real-time |
|---|---|---|
authentication |
create (login) | No |
shape-storage |
find, get, create, patch, remove | Yes |
shape-storage-record |
find, get, create, patch, remove | Yes |
static-record-store |
find, get, create, patch, remove | Yes |
job-service |
find, get, create, patch, remove | Yes |
job-service-reaction |
find, get, create, patch | Yes |
job-service-reaction-run |
find, get | Yes |
job-service-reaction-run-stream |
find, get | Yes |
access-point |
find, get, create, patch, remove | Yes |
vfs / virtual_fs |
find, get, create, patch, remove | Yes |
machines |
find, get, create, patch | Yes |
macros |
find, get, create, patch, remove | Yes |
bucket-registry |
find, get, create, patch, remove | Yes |
About
Roboteur is a hyper-automation + RPA platform by Saucecode. This skill covers the legacy (z1 / Feathers v4) platform generation — the integration surface, the shape and record-store formats, and the flow-export package format — so a Claude Code session can build integrations against a running instance or statically interpret exported automations for migration. Questions, issues and contributions: via the repository this skill is published from.