Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .changeset/v11-remove-http-node-aliases.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
"@objectstack/spec": major
"@objectstack/service-automation": major
---

Remove the deprecated `http_request` / `http_call` / `webhook` flow-node aliases — author `http` (ADR-0018 M3).

ADR-0018 M3 collapsed the divergent outbound-callout verbs onto the canonical
`http` node and kept the old names as deprecated aliases for back-compat. This
removes those aliases (the 11.0 cleanup):

- `http_request` is dropped from `FlowNodeAction` (and therefore
`FLOW_BUILTIN_NODE_TYPES`); authoring it now fails fast at parse instead of
resolving to `http`.
- `AutomationEngine` no longer registers the `http_request` / `http_call` /
`webhook` node aliases; only `http` is registered.
- The flow-builder palette offers `http`.

**Breaking.** Flows / workflow rules / approval actions that still use the old
node type must switch to `type: 'http'` (behavior is identical — durable outbox
when `config.durable`, inline fetch otherwise). The trigger `eventType: 'webhook'`
and the `webhook` resume event are unaffected — only the HTTP *node* aliases are
removed. First-party examples (showcase, app-crm) are migrated.
4 changes: 2 additions & 2 deletions examples/app-crm/src/flows/lead-qualification.flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { Flow } from '@objectstack/spec/automation';
* Exercises the full breadth of the Flow node type repertoire:
* • assignment — initialise scoring variables
* • get_record — fetch the related Account
* • http_request — external enrichment API (with fault edge → error handler)
* • http — external enrichment API (with fault edge → error handler)
* • script — calculate a composite lead score
* • decision — branch on score threshold (conditional + isDefault edges)
* • parallel_gateway — AND-split: notify rep AND create Opportunity simultaneously
Expand Down Expand Up @@ -106,7 +106,7 @@ export const LeadQualificationFlow: Flow = {
// A fault edge connects this node to error_handler if it fails.
{
id: 'enrich_lead',
type: 'http_request',
type: 'http',
label: 'Enrich Lead (External API)',
config: {
method: 'POST',
Expand Down
4 changes: 2 additions & 2 deletions examples/app-showcase/src/flows/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -824,7 +824,7 @@ export const ResilientSyncFlow = defineFlow({
nodes: [
{
id: 'push',
type: 'http_request',
type: 'http',
label: 'Push to CRM',
config: {
url: 'https://api.example.com/v1/tasks',
Expand Down Expand Up @@ -1009,7 +1009,7 @@ export const ProjectEscalationFlow = defineFlow({
retry: { maxRetries: 2, retryDelayMs: 500, backoffMultiplier: 2 },
errorVariable: '$error',
try: {
nodes: [{ id: 'push', type: 'http_request', label: 'POST incident', config: { url: 'https://api.example.com/v1/incidents', method: 'POST', body: { project: '{record.id}', severity: 'critical' } } }],
nodes: [{ id: 'push', type: 'http', label: 'POST incident', config: { url: 'https://api.example.com/v1/incidents', method: 'POST', body: { project: '{record.id}', severity: 'critical' } } }],
edges: [],
},
catch: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ import type { AutomationEngine, ConnectorActionContext } from '../engine.js';
/**
* Connector built-in node — `connector_action` (generic integration dispatch).
*
* Part of the platform baseline alongside `http_request` (ADR-0018 §Addendum):
* where `http_request` calls *any raw URL*, `connector_action` invokes *any
* Part of the platform baseline alongside `http` (ADR-0018 §Addendum):
* where `http` calls *any raw URL*, `connector_action` invokes *any
* registered connector's action*. The platform ships the generic dispatch node
* + an (initially empty) connector registry on the engine; concrete connectors
* — `connector-rest`, `connector-slack`, `connector-salesforce`, … — populate
* the registry at runtime via `engine.registerConnector()`.
*
* Because the registry starts empty, a flow referencing a connector that no
* plugin has registered fails the *step* with a clear error rather than failing
* to register — graceful degradation matching `http_request`'s fail-soft style.
* to register — graceful degradation matching `http`'s fail-soft style.
*/
export function registerConnectorNodes(engine: AutomationEngine, ctx: PluginContext): void {
engine.registerNodeExecutor({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function createCtx(messaging?: HttpSurface) {
} as any;
}

function httpFlow(type: 'http' | 'http_request' | 'http_call' | 'webhook', config: Record<string, unknown>) {
function httpFlow(type: 'http', config: Record<string, unknown>) {
return {
name: 'http_flow',
label: 'HTTP Flow',
Expand All @@ -50,7 +50,7 @@ function httpFlow(type: 'http' | 'http_request' | 'http_call' | 'webhook', confi
};
}

describe('http (canonical node) + deprecated aliases', () => {
describe('http (canonical node)', () => {
it('publishes a builtin io descriptor flagged needsOutbox', () => {
const engine = new AutomationEngine(createTestLogger());
registerHttpNodes(engine, createCtx());
Expand All @@ -61,14 +61,12 @@ describe('http (canonical node) + deprecated aliases', () => {
expect(d?.paradigms).toEqual(expect.arrayContaining(['flow', 'approval']));
});

it('registers http_request/http_call/webhook as deprecated aliases of http', () => {
it('does NOT register the removed http_request/http_call/webhook aliases (11.0)', () => {
const engine = new AutomationEngine(createTestLogger());
registerHttpNodes(engine, createCtx());
for (const alias of ['http_request', 'http_call', 'webhook']) {
expect(engine.getRegisteredNodeTypes()).toContain(alias);
const d = engine.getActionDescriptor(alias);
expect(d?.deprecated).toBe(true);
expect(d?.aliasOf).toBe('http');
for (const removed of ['http_request', 'http_call', 'webhook']) {
expect(engine.getRegisteredNodeTypes()).not.toContain(removed);
expect(engine.getActionDescriptor(removed)).toBeUndefined();
}
});

Expand Down Expand Up @@ -149,13 +147,5 @@ describe('http (canonical node) + deprecated aliases', () => {
expect(result.error).toContain('url');
});

it('a legacy http_request node still runs (via the alias → http)', async () => {
const engine = new AutomationEngine(createTestLogger());
registerHttpNodes(engine, createCtx());
engine.registerFlow('http_flow', httpFlow('http_request', { url: 'https://legacy.test', method: 'GET' }));
const result = await engine.execute('http_flow');
expect(result.success).toBe(true);
expect(fetchMock).toHaveBeenCalledOnce();
});
});
});
15 changes: 4 additions & 11 deletions packages/services/service-automation/src/builtin/http-nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@ import type { AutomationEngine } from '../engine.js';
import { interpolate } from './template.js';

/**
* HTTP built-in node — canonical `http` (ADR-0018 M3) + deprecated aliases.
* HTTP built-in node — canonical `http` (ADR-0018 M3).
*
* `http` is the single outbound-callout verb the platform offers Flow, Workflow
* Rules and Approval. It replaces the five divergent names (`http_request` /
* `http_call` / `webhook` / …) which are kept as **deprecated aliases** of
* `http` for back-compat (registered via {@link AutomationEngine.registerNodeAlias}).
* Rules and Approval. It supersedes the older divergent names (`http_request` /
* `http_call` / `webhook`), which were removed in 11.0 — author `http`.
*
* Two execution modes:
*
Expand Down Expand Up @@ -159,13 +158,7 @@ export function registerHttpNodes(engine: AutomationEngine, ctx: PluginContext):
},
});

// ADR-0018 M3: collapse the divergent outbound verbs onto `http`. Old saved
// flows / workflow rules / approval actions keep running via these aliases.
engine.registerNodeAlias('http_request', HTTP_TYPE, { name: 'HTTP Request', needsOutbox: true });
engine.registerNodeAlias('http_call', HTTP_TYPE, { name: 'HTTP Call', needsOutbox: true });
engine.registerNodeAlias('webhook', HTTP_TYPE, { name: 'Webhook', needsOutbox: true });

ctx.logger.info('[HTTP] http executor registered (+ deprecated aliases: http_request, http_call, webhook)');
ctx.logger.info('[HTTP] http executor registered');
}

/** Read a response body as JSON, falling back to text (empty body → null). */
Expand Down
4 changes: 2 additions & 2 deletions packages/services/service-automation/src/builtin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@
* - logic — try_catch (structured try/catch/retry, ADR-0031)
* - data — get/create/update/delete_record (platform CRUD baseline)
* - human — screen / script (core flow capability)
* - io — http_request (foundational outbound I/O)
* - io — http (foundational outbound I/O)
* - io — connector_action (generic integration dispatch)
* - io — notify (outbound notification via messaging service)
*
* `connector_action` is the *generic dispatch* counterpart to `http_request`
* `connector_action` is the *generic dispatch* counterpart to `http`
* (ADR-0018 §Addendum): the platform ships the node + an (initially empty)
* connector registry on the engine, and *concrete* connectors populate it at
* runtime via `engine.registerConnector()`. Third-party node types continue to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ function toStringList(value: unknown): string[] {
/**
* `notify` built-in node (ADR-0012) — outbound notification.
*
* Baseline node and the human-notification counterpart to `http_request`
* Baseline node and the human-notification counterpart to `http`
* ("raw call") and `connector_action` ("call a registered integration"):
* `notify` hands a topic + recipients + message to the platform's messaging
* service, which fans it out across the user's channels (inbox by default).
Expand Down
12 changes: 6 additions & 6 deletions packages/services/service-automation/src/engine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -862,9 +862,9 @@ describe('AutomationServicePlugin (Kernel Integration)', () => {
expect(nodeTypes).toContain('script');

// HTTP node (foundational I/O)
expect(nodeTypes).toContain('http_request');
expect(nodeTypes).toContain('http');

// connector_action is the generic-dispatch sibling of http_request and is
// connector_action is the generic-dispatch sibling of http and is
// baseline (ADR-0018 §Addendum): the engine ships the node + an empty
// connector registry; concrete connectors are plugins.
expect(nodeTypes).toContain('connector_action');
Expand Down Expand Up @@ -1067,9 +1067,9 @@ describe('Built-in HTTP node', () => {
engine = kernel.getService<AutomationEngine>('automation');
});

it('should register the http_request node type', () => {
it('should register the http node type', () => {
const types = engine.getRegisteredNodeTypes();
expect(types).toContain('http_request');
expect(types).toContain('http');
});

it('should register connector_action in the built-in baseline', () => {
Expand Down Expand Up @@ -2175,11 +2175,11 @@ describe('Action Descriptor Registry (ADR-0018)', () => {
expect(byType.has('decision')).toBe(true);
expect(byType.get('decision')?.category).toBe('logic');
expect(byType.get('create_record')?.category).toBe('data');
expect(byType.get('http_request')?.category).toBe('io');
expect(byType.get('http')?.category).toBe('io');

// All built-ins are tagged source: 'builtin'.
expect(byType.get('decision')?.source).toBe('builtin');
expect(byType.get('http_request')?.source).toBe('builtin');
expect(byType.get('http')?.source).toBe('builtin');
});
});

Expand Down
2 changes: 1 addition & 1 deletion packages/services/service-automation/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export interface AutomationServicePluginOptions {
* Responsibilities:
* 1. init phase: Create engine instance, register as 'automation' service, and
* seed the platform's built-in node executors (logic / CRUD / screen-script /
* http_request) via {@link installBuiltinNodes}. Per ADR-0018, foundational
* http) via {@link installBuiltinNodes}. Per ADR-0018, foundational
* capabilities are built into the core, not packaged as optional plugins.
* 2. start phase: Trigger 'automation:ready' hook so third-party plugins can
* register additional node types, then pull flow definitions from the
Expand Down
2 changes: 1 addition & 1 deletion packages/spec/src/automation/bpmn-interop.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe('BpmnElementMappingSchema', () => {
it('should accept mapping with notes', () => {
const mapping = BpmnElementMappingSchema.parse({
bpmnType: 'bpmn:serviceTask',
flowNodeAction: 'http_request',
flowNodeAction: 'http',
bidirectional: true,
notes: 'Maps HTTP/connector tasks',
});
Expand Down
2 changes: 1 addition & 1 deletion packages/spec/src/automation/bpmn-interop.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ export const BUILT_IN_BPMN_MAPPINGS: BpmnElementMapping[] = [
{ bpmnType: 'bpmn:endEvent', flowNodeAction: 'end', bidirectional: true },
{ bpmnType: 'bpmn:exclusiveGateway', flowNodeAction: 'decision', bidirectional: true },
{ bpmnType: 'bpmn:parallelGateway', flowNodeAction: 'parallel_gateway', bidirectional: true },
{ bpmnType: 'bpmn:serviceTask', flowNodeAction: 'http_request', bidirectional: true, notes: 'Maps HTTP/connector tasks' },
{ bpmnType: 'bpmn:serviceTask', flowNodeAction: 'http', bidirectional: true, notes: 'Maps HTTP/connector tasks' },
{ bpmnType: 'bpmn:scriptTask', flowNodeAction: 'script', bidirectional: true },
{ bpmnType: 'bpmn:userTask', flowNodeAction: 'screen', bidirectional: true },
{ bpmnType: 'bpmn:callActivity', flowNodeAction: 'subflow', bidirectional: true },
Expand Down
4 changes: 2 additions & 2 deletions packages/spec/src/automation/bpmn-mapping.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ describe('exportConstructsToBpmn — try_catch', () => {
nodes: [
node('start', 'start'),
node('tc', 'try_catch', {
try: { nodes: [node('charge', 'http_request')], edges: [] },
try: { nodes: [node('charge', 'http')], edges: [] },
catch: { nodes: [node('flag', 'update_record')], edges: [] },
errorVariable: '$error',
retry: { maxRetries: 2, retryDelayMs: 100 },
Expand Down Expand Up @@ -179,7 +179,7 @@ describe('importBpmnToConstructs — foreign BPMN (no markers)', () => {

it('warns (does not fold) a foreign boundary_event with no marker', () => {
const foreign: MappableFlow = {
nodes: [node('host', 'http_request'), node('b', BPMN_BOUNDARY_EVENT)],
nodes: [node('host', 'http'), node('b', BPMN_BOUNDARY_EVENT)],
edges: [],
};
const { diagnostics, unmappedCount } = importBpmnToConstructs(foreign);
Expand Down
6 changes: 3 additions & 3 deletions packages/spec/src/automation/execution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ describe('ExecutionStepLogSchema', () => {

it('should accept a step log with error details', () => {
const step = ExecutionStepLogSchema.parse({
nodeId: 'http_request',
nodeType: 'http_request',
nodeId: 'http',
nodeType: 'http',
nodeLabel: 'Call External API',
status: 'failure',
startedAt: '2026-02-01T10:00:00Z',
Expand Down Expand Up @@ -178,7 +178,7 @@ describe('ExecutionErrorSchema', () => {
const error = ExecutionErrorSchema.parse({
id: 'err_001',
executionId: 'exec_001',
nodeId: 'http_request',
nodeId: 'http',
severity: 'error',
code: 'HTTP_TIMEOUT',
message: 'Request timed out after 30s',
Expand Down
2 changes: 1 addition & 1 deletion packages/spec/src/automation/execution.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export type ExecutionStatus = z.infer<typeof ExecutionStatus>;
*/
export const ExecutionStepLogSchema = lazySchema(() => z.object({
nodeId: z.string().describe('Node ID that was executed'),
nodeType: z.string().describe('Node action type (e.g., "decision", "http_request")'),
nodeType: z.string().describe('Node action type (e.g., "decision", "http")'),
nodeLabel: z.string().optional().describe('Human-readable node label'),
status: z.enum(['success', 'failure', 'skipped']).describe('Step execution result'),
startedAt: z.string().datetime().describe('When the step started'),
Expand Down
12 changes: 6 additions & 6 deletions packages/spec/src/automation/flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe('FlowNodeAction', () => {
const actions = [
'start', 'end', 'decision', 'assignment', 'loop',
'create_record', 'update_record', 'delete_record', 'get_record',
'http_request', 'notify', 'script', 'screen', 'wait', 'subflow', 'connector_action',
'http', 'notify', 'script', 'screen', 'wait', 'subflow', 'connector_action',
'parallel_gateway', 'join_gateway', 'boundary_event',
];

Expand Down Expand Up @@ -120,7 +120,7 @@ describe('FlowNodeSchema', () => {
const types = [
'start', 'end', 'decision', 'assignment', 'loop',
'create_record', 'update_record', 'delete_record', 'get_record',
'http_request', 'notify', 'script', 'screen', 'wait', 'subflow', 'connector_action',
'http', 'notify', 'script', 'screen', 'wait', 'subflow', 'connector_action',
'parallel_gateway', 'join_gateway', 'boundary_event',
] as const;

Expand All @@ -137,7 +137,7 @@ describe('FlowNodeSchema', () => {
it('should accept node with timeoutMs', () => {
const result = FlowNodeSchema.safeParse({
id: 'http_1',
type: 'http_request',
type: 'http',
label: 'Call API',
timeoutMs: 5000,
});
Expand Down Expand Up @@ -372,7 +372,7 @@ describe('FlowSchema', () => {
},
{
id: 'send_approval_request',
type: 'http_request',
type: 'http',
label: 'Send to Manager',
config: {
url: '/api/approvals',
Expand Down Expand Up @@ -528,7 +528,7 @@ describe('FlowSchema', () => {
{ id: 'start', type: 'start', label: 'Start' },
{
id: 'call_external_api',
type: 'http_request',
type: 'http',
label: 'Call External API',
config: {
url: 'https://api.external.com/v1/data',
Expand Down Expand Up @@ -1041,7 +1041,7 @@ describe('BPMN — Boundary Event', () => {
type: 'autolaunched',
nodes: [
{ id: 'start', type: 'start', label: 'Start' },
{ id: 'api_call', type: 'http_request', label: 'Call External API', timeoutMs: 5000 },
{ id: 'api_call', type: 'http', label: 'Call External API', timeoutMs: 5000 },
{
id: 'api_error_boundary',
type: 'boundary_event',
Expand Down
1 change: 0 additions & 1 deletion packages/spec/src/automation/flow.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ export const FlowNodeAction = z.enum([
'delete_record', // CRUD: Delete
'get_record', // CRUD: Get/Query
'http', // Outbound HTTP callout (ADR-0018 M3) — canonical; outbox-backed when durable
'http_request', // Deprecated alias of `http` (ADR-0018 M3)
'notify', // Outbound notification (ADR-0012) — dispatched via the messaging service
'script', // Custom action: a built-in side-effect (`config.actionType: 'email'|'slack'`)
// or a registered function (`config.function: 'name'` + `config.inputs`),
Expand Down
4 changes: 2 additions & 2 deletions packages/spec/src/automation/node-executor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,15 +177,15 @@ describe('NodeExecutorDescriptorSchema', () => {
const desc = NodeExecutorDescriptorSchema.parse({
id: 'objectstack:http-executor',
name: 'HTTP Request Executor',
nodeTypes: ['http_request', 'connector_action'],
nodeTypes: ['http', 'connector_action'],
version: '2.0.0',
description: 'Executes HTTP requests and connector actions',
supportsPause: false,
supportsCancellation: true,
supportsRetry: true,
configSchemaRef: '#/definitions/HttpExecutorConfig',
});
expect(desc.nodeTypes).toContain('http_request');
expect(desc.nodeTypes).toContain('http');
expect(desc.nodeTypes).toContain('connector_action');
expect(desc.supportsCancellation).toBe(true);
});
Expand Down
2 changes: 1 addition & 1 deletion packages/spec/src/automation/node-executor.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export type WaitExecutorConfig = z.infer<typeof WaitExecutorConfigSchema>;

/**
* Generic node executor plugin descriptor.
* Each node type (wait, script, http_request, etc.) can register
* Each node type (wait, script, http, etc.) can register
* a custom executor via this descriptor.
*/
export const NodeExecutorDescriptorSchema = lazySchema(() => z.object({
Expand Down
Loading