Skip to content

feat(automation): resolve & validate script-node callables; function registration (#1870) - #1903

Merged
os-zhuang merged 2 commits into
mainfrom
fix/1870-callable-validation
Jun 15, 2026
Merged

feat(automation): resolve & validate script-node callables; function registration (#1870)#1903
os-zhuang merged 2 commits into
mainfrom
fix/1870-callable-validation

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes#1870.

Problem

A flow script/action node pointing at an unregistered callable (invoke_function → a function defined nowhere), or declaring no actionType/function at all (actionType: undefined), built + typechecked fine and then silently did nothing at runtime. There was also no first-class way for an authored package to register a function a script node could call. Found while runtime-testing objectstack-ai/templates on 9.5.1 (helpdesk ai_triage_on_createhelpdesk.aiTriageStub; project daily_ai_risk_assessmentpm.aiRiskAssessmentStub; content publication_rollup with actionType: undefined).

Fix

Three surfaces, mirroring the loud-validation approach from #1877:

1. Loud runtime resolutionscreen-nodes.ts
The built-in script executor resolves its target in order:

  • built-in side-effect (email/slack, logger-backed, preserved) →
  • a registered function (config.function, or a bare config.actionType that matches no built-in, so a node pointed straight at helpdesk.aiTriageStub resolves) →
  • otherwise fail the step loudly.

The old [Script:…] (no-op handler) → { success: true } path is gone.

2. First-class registration pathengine.ts + plugin.ts
New AutomationEngine.setFunctionResolver() / resolveFunction(). The plugin's start() bridges it to ObjectQL's resolveFunction, populated from bundle.functions / defineStack({ functions }) — the same registry hooks/actions already use. So an authored package registers a function and calls it from a flow:

// defineStack({ functions: { 'helpdesk.aiTriageStub': (ctx) => ({ priority: 'high' }) }}){type: 'script',config: {function: 'helpdesk.aiTriageStub',inputs: {ticket: '{record.id}'}}}

The function receives { input, variables, automation, logger } and its return value becomes the node output. Reuses the existing script node (no enum/Studio churn) per the chosen authoring contract.

3. Build-time structural checkvalidate-expressions.ts
objectstack build now flags a script node that declares neither actionType nor function (the actionType: undefined repro). Function existence is not checkable at build — functions are code, not serialized into the artifact — so existence is enforced at runtime.

Design note

Registration-time (registerFlow) does not hard-fail on a target-less script node: a script node can be claimed by a plugin-registered custom executor (existing engine tests rely on this), and the engine can't distinguish built-in vs custom at registration. The guard lives where it's unambiguous — the built-in executor (runtime) and objectstack build (authored metadata).

Tests

  • New screen-nodes.test.ts: built-in side-effect, function resolution + output capture, bare-actionType shorthand, loud failure for unregistered function, loud failure for no target, thrown-function surfacing.
  • validate-expressions.test.ts: build flags target-less script node; accepts actionType/function.
  • @objectstack/service-automation 193 passed · @objectstack/cli 260 passed · production build + DTS clean (the package's pre-existing tsc --noEmit test-fixture errors are unrelated and present on main).

Scope

Slice A (loud validation) + Slice B (registration path) per the issue. connector_action already failed loud and is unchanged.

🤖 Generated with Claude Code

…registration path (#1870)
A flow `script` node pointing at an unregistered callable — or declaring no
`actionType`/`function` at all — built fine and silently did nothing at
runtime. Close the gap on three surfaces:
- Runtime: the built-in `script` executor resolves its target (built-in
side-effect → registered function → else fail loud). The old
`(no-op handler)` success path is removed, so an unwired callable can no
longer quietly skip the step.
- Registration path: add `AutomationEngine.setFunctionResolver()`/
`resolveFunction()` and bridge it in the plugin's `start()` to ObjectQL's
`resolveFunction` (fed by `bundle.functions` / `defineStack({ functions })`).
A `script` node can now invoke an authored function by name:
`{ type: 'script', config: { function: 'my_fn', inputs: { … } } }`.
- Build: `objectstack build` flags a `script` node that names no callable
(the `actionType: undefined` repro). Existence is checked at runtime —
functions are code, not serialized into the artifact.
Tests: new script-executor suite (function resolution, loud failure, bare
actionType shorthand, thrown-function surfacing) + CLI build-validation cases.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercelBot commented Jun 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
specReadyReadyPreview, CommentJun 15, 2026 9:32am

Request Review

… as an empty no-op (#1870)
CI Build Core caught that the build-time check (and the runtime executor) treated
a `script` node carrying inline JS in `config.script` as target-less, breaking
example-crm/showcase builds. Inline `config.script` is a distinct, recognized
form — the built-in runtime has no server-side JS sandbox so it does NOT execute
it (a separate gap, out of #1870's callable-resolution scope). Now:
- build validation accepts `function` | `actionType` | non-empty `script`;
- the runtime executor warns loudly that inline scripts aren't executed (and
points to `defineStack({ functions })`) instead of failing the step or
silently succeeding.
Verified example-crm, app-showcase, app-todo all build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@os-zhuang
os-zhuang merged commit dc8b2de into mainJun 15, 2026
13 checks passed
@os-zhuang
os-zhuang deleted the fix/1870-callable-validation branch June 15, 2026 09:33
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[P1] Flow action/script nodes pointing to unregistered callables silently no-op; no template fn-registration path

1 participant

@os-zhuang