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

vercel Bot commented Jun 15, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jun 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 main Jun 15, 2026
13 checks passed
@os-zhuang
os-zhuang deleted the fix/1870-callable-validation branch June 15, 2026 09:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

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