objectstack-ai/objectstack/objectstack-automation
Claude Code and agent skill by objectstack-ai/objectstack
- community
- ext:[{"type":"hook","description":"Pre/post lifecycle hooks"}]
Checking that the SKILL.md file is still in the repository.
Design ObjectStack automation — Flows (visual logic), Triggers, Approvals, state machines, and the `jobs` (`defineJob`) / `webhooks` (`defineWebhook`) stack collections. Use when the user is adding `*.flow.ts`, wiring an event-driven rule, modelling an approval chain, or building an interactive screen flow / wizard (objectstack-ui routes those here). Do not use for data lifecycle hooks at the object layer (see objectstack-data) or for kernel / plugin events (see objectstack-platform). CEL expre…
Install
npx vskill@latest install objectstack-ai/objectstack/objectstack-automationSemi-extensibleMentions customization, but needs skill-specific knowledge. Read the extensible skills standard
SKILL.md
Automation Design — ObjectStack Automation Protocol
When to Use This Skill
Flows (auto-launched, screen, scheduled), state machines and approvals, record-change triggers, vendor integrations and their callbacks, and assignment rules.
Predicates and conditions are CEL — everycondition/guard/entryCondition/ filtervalueis an Expression envelope: a plain CELstring, or theP\...\`/cel\...\` tag (author-time validation); pick oneper file. Contract, stdlib, legacy → CEL table: objectstack-formula.
Flows — Visual Logic Orchestration
Flow Types
| Type | When to Use | |
|---|---|---|
autolaunched | Runs without user interaction — triggered by events, APIs, or other flows | |
screen | Interactive — presents UI screens to the user (wizards, forms) | |
schedule | Runs on a cron/interval cadence declared on the start node's `config.schedule` — or a per-record date sweep via config.timeRelative, see Time-relative triggers | |
record_change | Fires automatically on record create/update/delete. autolaunched + the same record-* binding behaves identically; record_change also opts into the trigger-readiness lint | |
api | Inbound webhook — needs a start-node secretRef (see Inbound webhook triggers below); a flow only ever started explicitly is autolaunched |
A flow an installed package ships is read-only. To change what it does, save a same-named flow in the base's paired .custom package declaring overrides: { package: '<base>', basedOn: '<base version>' } (objectstack-platform § Customizing an installed package): it is armed in the base's place, keeps the base's name and its one activation switch (POST /api/v1/automation/:name/toggle, { enabled: false } = off for this deployment), and a run suspended on the old body finishes on that body. An undeclared same-named flow in the pair or in a dependent is refused at publish. A flow beside it is "new similar": POST /api/v1/automation/:name/clone { name, label, package? } lands the copy switched off in the pair, never package-less; a flow holding a secret / signingSecret is refused (409): author it anew.
Flow Node Types
Flows are built from built-in node types (plugins register more):
Control Flow
| Node | Purpose | |
|---|---|---|
start | Entry point — every flow has exactly one | |
end | Exit point — can have multiple (early exit, error exit) | |
decision | Conditional branching — routed by edge `condition` predicates, not node config | |
loop | Iterate a nested `config.body` region once per item of config.collection — a CEL value envelope ({ dialect: 'cel', source: 'rows' }) or an inline list, never a string; iteratorVariable (default item) and optional indexVariable bind inside it, maxIterations caps it | |
parallel | Fan out into config.branches[] (≥ 2 regions) run concurrently, joined implicitly at block end — no split/join pair to mis-wire | |
try_catch | Run config.try; on failure run config.catch with the error in errorVariable (default $error); config.retry re-runs try with backoff first. No `finally` — the container's ordinary out-edges are the continuation | |
map | Sequential multi-instance — invoke a subflow once per item of config.collection (shaped as loop's); each iteration may pause (batch approvals) | |
wait | Pause execution until a timer elapses or a named signal arrives | |
subflow | Invoke another flow (reusable composition) | |
parallel_gateway / join_gateway / boundary_event | Not author-facing — BPMN-interop forms a parallel / try_catch container lowers into; author the container |
Data Operations
| Node | Purpose | |
|---|---|---|
assignment | Set variable values | |
create_record | Insert a new record | |
update_record | Modify existing records | |
delete_record | Remove records | |
get_record | Fetch records with filters — there is no `query_record` node (that name has no executor and throws) | |
save_notice | Tell the saver what this create/update started — record create/update trigger only, not after a pause: objectstack-data rules/lifecycle.md § Save Notice |
External Integration
| Node | Purpose | |||
|---|---|---|---|---|
http | Call an external HTTP API — always set timeoutMs (unset = no timeout). url and headers are served as written with the definition to every member who can read flows — only signingSecret and a start node's secret are withheld — so never a credential there: a credential means a declared connector called via connector_action | |||
notify | Send a notification through the messaging service | |||
connector_action | Dispatch an action of a declared connector (§ Integrations are declared, not coded) or a plugin's. connectorConfig.input is a value-slot map like subflow input. Once evaluated, it is judged against the action's inputSchema (required, type, properties, items) before dispatch: a breach fails the step VALIDATION_ERROR (fault-routable), nothing sent; os validate does not judge the inputSchema fit yet | |||
script | Call a registered function named by config.function (see Valid-but-silently-wrong #3) | |||
screen | Display a UI form to the user (screen flows only). config.fields[]: { name, label, type, required, options, defaultValue }, type an open widget hint; 'file' renders the upload widget and resumes with the sys_file id bound to name — a later create_record / update_record writes it into a file field, which claims the file: collect first, write after the last screen. 'record_link' (never 'record', an object field type) takes no input — it shows the reference record whose id defaultValue evaluates to, by display name, as a link to its record page: the result screen of a flow that created records, { name: 'account_link', type: 'record_link', reference: 'crm_account', defaultValue: { dialect: 'cel', source: 'account.id' } } over a create_record outputVariable: 'account'. Both keys required; required / options / placeholder / min / max refused; a resume value under its name refused (400 INVALID_SCREEN_INPUT). A completed end message is refused and a markdown link in a screen description renders literally. Next / Submit and "Step N of M" are not authored: the engine reads the graph when the screen pauses and serves progress (`nextScreen: 'none' | 'possible' | 'certain'; step / stepCount only when every path agrees), which alone labels the button and draws the counter. A decision, fault edge, loop back, non-screen pause (approval, wait) or subflow after a screen answers possible — "Next", no count. progress in a node config is refused; never hand-roll a button label or step counter into fields` |
Human Decision
| Node | Purpose | |
|---|---|---|
approval | Route a record for human sign-off — suspends the run until a decision, then continues down the approve / reject branch |
notify — the most-used node type
NotifyConfigSchema is strictObject — an undeclared key is a named parse error. RAW keys are read as written: a {token} in one is refused.
An option label in a notice: {{ stage | option:opportunity.stage }} in the mail template (each recipient's language; mail-only, refused in inline title / message) — never label() in templateData, evaluated once per run, before fan-out.
Flow Variables
variables is an array of { name, type, isInput, isOutput } entries — not a name-keyed map; a variable has no label: variables: [{ name: 'case_id', type: 'text', isInput: true }, { name: 'approval_result', type: 'boolean', isOutput: true }].
→ references/examples-flows.md § Flow Example — Auto-Escalate Overdue Cases.
Failure routing & runAs
Handling a failed node: a `fault` edge.{ source, target, type: 'fault' }routes a failed node to a handler instead of ending the run. **type: 'fault'is what routes — alabel: 'error'alone does nothing:** an ordinary edgetraverses on SUCCESS (objectstack validatereportsflow-error-label-not-fault).A handled failure does not consume a flow-levelerrorHandling.retry, whichreplays from the start — prefer a fault edge for a local failure. The handlerreads{{ <nodeId>.error }}(run-wide{{ $error.message }}; in CEL,vars["$error"]); the run then reports success and the failed step stays in the trace.
| ROUTES (runtime failure) | Does NOT route (fatal either way) | |
|---|---|---|
| 404, rate-limit, rejected write, failed subflow | missing required config key (objectName, url, flowName, connectorId/actionId); filter operand that evaluated to null or a non-value; graph past the nesting ceiling; unscoped run |
Writing a `readonly` field? Set `runAs: 'system'`. Under the defaultrunAs: 'user'the engine strips it fromcreate_record/update_record— the step succeeds, the value never lands, the drop is named in the step'swarnings — so a flow maintaining one (approval stamps, SLA markers, rollups)runsrunAs: 'system';os validate/os buildfail arunAs:'user'update_recordwriting one (readonlyWhen: a warning). Do not removereadonlyinstead.
Elevate the write, not the flow. Ascreenflow staysrunAs: 'user'; astep that must write areadonlyfield moves into a dedicatedrunAs: 'system'flow called from asubflownode — raising the whole flow silently elevatesevery other write in it.Elevation stops at identity assignment. ArunAs: 'system'write (insertthrough purge) ofsys_user_position,sys_user_permission_set,sys_invitationorsys_memberis refusedPERMISSION_DENIED, a systemsubflow included. Grant positions and permission sets from arunAs: 'user'flow — the delegated-administration gate judges that user and records them asgranted_by; invitations and memberships change through the organizationendpoints (row 1b, https://objectstack.ai/docs/permissions/system-context#1-authorization-and-scoping).A `runAs: 'system'` sweep must pin its organization. System context has notrigger user, so a scan or rollup with no organization predicate reads andwrites across every tenant; filter on the platform-injected tenant column, neverre-declare it per object.
This is the start of the file. Read the full SKILL.md on GitHub.
Security
- Security scanPassed, 95 of 100Pattern checks on the skill's files.
- Model reviewPassed
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingskills/objectstack-automation/SKILL.md
Popularity
- Repository stars
- 76
- Repository forks
- 20
- 7-day trend score
- 0
Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.
Extension points
hookPre/post lifecycle hooks
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/objectstack-ai/objectstack/objectstack-automation)