@amalgm/automations 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AXIOMS.md CHANGED
@@ -41,7 +41,9 @@
41
41
  state; it never owns a second automation-definition API.
42
42
  15. Admitting a run atomically verifies the current enabled configuration,
43
43
  stores a secret-free snapshot, and, for a schedule, advances exactly the
44
- firing instant that was claimed.
44
+ firing instant that was claimed. Manual, schedule, and webhook admission
45
+ derive executability from the same declared set of supported workflow
46
+ versions.
45
47
  16. Legacy local automation storage is not a compatibility authority. Engine
46
48
  is deprecated read-only evidence; active callers use this SDK and its
47
49
  standalone service, and no old store may run beside them.
@@ -123,3 +125,7 @@
123
125
  credential, tunnel credential, or a standalone Automations authorization
124
126
  value. Native tool authentication comes from that tool's own user-level
125
127
  setup, not from Amalgm's private transport authority.
128
+ 43. The durable step journal has one SDK-owned serialized size bound. Oversized
129
+ action or process results become an explicit byte count plus bounded JSON
130
+ preview before transport, so output can never strand a run outside the
131
+ ledger it is meant to update.
package/PURPOSE.md CHANGED
@@ -58,7 +58,9 @@ the selected machine host supplies every effectful capability.
58
58
 
59
59
  The machine records a step as running before invoking it and records its output
60
60
  before advancing. Reconnect resumes at the first step that is not already
61
- complete. A transient network failure releases the same run back to the queue
61
+ complete. One serialized journal bound applies to action and process output;
62
+ oversized results retain their byte count and a bounded preview, so evidence
63
+ cannot make its own durable update impossible. A transient network failure releases the same run back to the queue
62
64
  with a bounded future retry time; a configuration, authorization, or execution
63
65
  error fails it. Every execution capability receives a stable per-step
64
66
  idempotency key. Amalgm actions can use it to make an uncertain acknowledgement
package/README.md CHANGED
@@ -169,7 +169,10 @@ newline. Either action `input` or process `stdin` may contain the exact template
169
169
  Shell injects action and process capabilities into the shared executor. Process
170
170
  results record exit code, signal, stdout, stderr, and truncation in run history;
171
171
  timeouts, cancellation, and output limits are enforced on the selected
172
- machine. Children inherit the user's ordinary machine environment so installed
172
+ machine. The durable journal begins compacting after 1 MiB and stores oversized
173
+ results as an explicit original byte count plus a bounded JSON preview, keeping
174
+ every run update within the service transport bound. Children inherit the
175
+ user's ordinary machine environment so installed
173
176
  CLI auth continues to work, but never inherit Shell's private runtime, tunnel,
174
177
  or machine credentials. Values in workflow `env` are persisted non-secret
175
178
  configuration.
@@ -1,4 +1,5 @@
1
1
  import { automationCommandDefinition, } from '../command-surface.js';
2
+ import { MAX_CLI_RUNTIME_RESPONSE_BYTES } from '../execution-limits.js';
2
3
  import { AutomationError, ValidationError } from '../errors.js';
3
4
  export function createShellAutomationCommands(options) {
4
5
  const endpoint = shellAutomationEndpoint(options.runtimeUrl);
@@ -68,21 +69,49 @@ export function shellAutomationEndpoint(runtimeUrl) {
68
69
  async function jsonResponse(response) {
69
70
  let bytes;
70
71
  try {
71
- bytes = new Uint8Array(await response.arrayBuffer());
72
+ bytes = await boundedResponse(response, MAX_CLI_RUNTIME_RESPONSE_BYTES);
72
73
  }
73
74
  catch (error) {
75
+ if (error instanceof AutomationError)
76
+ throw error;
74
77
  throw new AutomationError('runtime_transport', `Cannot read Amalgm Shell runtime response: ${message(error)}`);
75
78
  }
76
- if (bytes.byteLength > 2 * 1024 * 1024) {
77
- throw new AutomationError('runtime_protocol', 'Amalgm Shell runtime response is too large');
78
- }
79
79
  try {
80
- return JSON.parse(Buffer.from(bytes).toString('utf8'));
80
+ return JSON.parse(bytes.toString('utf8'));
81
81
  }
82
82
  catch {
83
83
  return null;
84
84
  }
85
85
  }
86
+ async function boundedResponse(response, maximumBytes) {
87
+ const declared = Number(response.headers.get('content-length'));
88
+ if (Number.isFinite(declared) && declared > maximumBytes) {
89
+ throw new AutomationError('runtime_protocol', 'Amalgm Shell runtime response is too large');
90
+ }
91
+ if (!response.body)
92
+ return Buffer.alloc(0);
93
+ const chunks = [];
94
+ let size = 0;
95
+ const reader = response.body.getReader();
96
+ try {
97
+ while (true) {
98
+ const { done, value } = await reader.read();
99
+ if (done)
100
+ break;
101
+ const chunk = Buffer.from(value);
102
+ size += chunk.byteLength;
103
+ if (size > maximumBytes) {
104
+ await reader.cancel();
105
+ throw new AutomationError('runtime_protocol', 'Amalgm Shell runtime response is too large');
106
+ }
107
+ chunks.push(chunk);
108
+ }
109
+ }
110
+ finally {
111
+ reader.releaseLock();
112
+ }
113
+ return Buffer.concat(chunks, size);
114
+ }
86
115
  function toolResult(value) {
87
116
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
88
117
  throw new AutomationError('runtime_protocol', 'Automations MCP returned an invalid tool result');
@@ -0,0 +1,6 @@
1
+ /** Start compacting step output before metadata can approach the service body bound. */
2
+ export declare const RUN_JOURNAL_OUTPUT_BUDGET_BYTES: number;
3
+ /** One persisted journal, including step metadata, must remain below 1.5 MiB. */
4
+ export declare const MAX_RUN_JOURNAL_BYTES: number;
5
+ /** A get response can contain the default page of twenty maximum-sized journals. */
6
+ export declare const MAX_CLI_RUNTIME_RESPONSE_BYTES: number;
@@ -0,0 +1,6 @@
1
+ /** Start compacting step output before metadata can approach the service body bound. */
2
+ export const RUN_JOURNAL_OUTPUT_BUDGET_BYTES = 1024 * 1024;
3
+ /** One persisted journal, including step metadata, must remain below 1.5 MiB. */
4
+ export const MAX_RUN_JOURNAL_BYTES = 1536 * 1024;
5
+ /** A get response can contain the default page of twenty maximum-sized journals. */
6
+ export const MAX_CLI_RUNTIME_RESPONSE_BYTES = 32 * 1024 * 1024;
@@ -5,6 +5,7 @@ export { createAutomationApi, type AuthenticateAutomationRequest, type Automatio
5
5
  export { automationCliHelp, createShellAutomationCommands, runAutomationCli, runAutomationCliMain, shellAutomationEndpoint, type AutomationCliIo, type AutomationCliMainOptions, type ShellAutomationCommandOptions, } from './cli.js';
6
6
  export { automationCommandDefinition, automationCommandDefinitions, createAutomationCommands, type AutomationCommandAnnotations, type AutomationCommandDefinition, type AutomationCommandName, type AutomationCommands, } from './command-surface.js';
7
7
  export { automationFailure, type AutomationFailureEnvelope } from './command-result.js';
8
+ export { MAX_CLI_RUNTIME_RESPONSE_BYTES, MAX_RUN_JOURNAL_BYTES, RUN_JOURNAL_OUTPUT_BUDGET_BYTES, } from './execution-limits.js';
8
9
  export { createAutomationMcpServer } from './mcp.js';
9
10
  export { createAutomationToolSurface, type AutomationToolSurface, type AutomationView, type CreateAutomationDefinition, type UpdateAutomationDefinition, type WorkflowChange, } from './tool-surface.js';
10
11
  export { SupabaseAutomationCrudRepository, type SupabaseRpcClient } from './supabase-crud.js';
package/dist/src/index.js CHANGED
@@ -12,6 +12,7 @@ export { createAutomationApi } from './http.js';
12
12
  export { automationCliHelp, createShellAutomationCommands, runAutomationCli, runAutomationCliMain, shellAutomationEndpoint, } from './cli.js';
13
13
  export { automationCommandDefinition, automationCommandDefinitions, createAutomationCommands, } from './command-surface.js';
14
14
  export { automationFailure } from './command-result.js';
15
+ export { MAX_CLI_RUNTIME_RESPONSE_BYTES, MAX_RUN_JOURNAL_BYTES, RUN_JOURNAL_OUTPUT_BUDGET_BYTES, } from './execution-limits.js';
15
16
  export { createAutomationMcpServer } from './mcp.js';
16
17
  export { createAutomationToolSurface, } from './tool-surface.js';
17
18
  export { SupabaseAutomationCrudRepository } from './supabase-crud.js';
package/dist/src/mcp.js CHANGED
@@ -4,7 +4,14 @@ import { automationCommandDefinitions, createAutomationCommands, } from './comma
4
4
  import { automationFailure } from './command-result.js';
5
5
  import { createAutomationToolSurface } from './tool-surface.js';
6
6
  export { createAutomationToolSurface };
7
- const resultSchema = { result: z.unknown() };
7
+ const resultSchema = {
8
+ result: z.unknown().optional(),
9
+ error: z.object({
10
+ code: z.string(),
11
+ message: z.string(),
12
+ status: z.number().optional(),
13
+ }).strict().optional(),
14
+ };
8
15
  export function createAutomationMcpServer(sdk) {
9
16
  const server = new McpServer({ name: 'amalgm-automations-mcp-server', version: '0.1.0' });
10
17
  const commands = createAutomationCommands(sdk);
@@ -1,9 +1,13 @@
1
+ import { MAX_RUN_JOURNAL_BYTES, RUN_JOURNAL_OUTPUT_BUDGET_BYTES, } from './execution-limits.js';
2
+ const MAX_OUTPUT_PREVIEW_CHARACTERS = 64 * 1024;
1
3
  export function emptyJournal() {
2
4
  return { version: 1, steps: [] };
3
5
  }
4
6
  export function runJournal(value, plan) {
5
7
  if (value === undefined)
6
8
  return emptyJournal();
9
+ if (jsonBytes(value) > MAX_RUN_JOURNAL_BYTES)
10
+ throw new Error('Run step journal exceeds its persistence bound');
7
11
  if (!value || typeof value !== 'object' || Array.isArray(value))
8
12
  throw new Error('Run step journal is invalid');
9
13
  const source = value;
@@ -34,12 +38,63 @@ export function putStep(journal, step) {
34
38
  steps.push(step);
35
39
  else
36
40
  steps[index] = step;
37
- return { version: 1, steps };
41
+ let candidate = { version: 1, steps };
42
+ if (jsonBytes(candidate) > RUN_JOURNAL_OUTPUT_BUDGET_BYTES && step.output !== undefined) {
43
+ const compacted = { ...step, output: compactOutput(step.output) };
44
+ if (index === -1)
45
+ steps[steps.length - 1] = compacted;
46
+ else
47
+ steps[index] = compacted;
48
+ candidate = { version: 1, steps };
49
+ }
50
+ if (jsonBytes(candidate) > MAX_RUN_JOURNAL_BYTES) {
51
+ candidate = removeStoredPreviews(candidate);
52
+ }
53
+ if (jsonBytes(candidate) > MAX_RUN_JOURNAL_BYTES) {
54
+ throw new Error('Run step journal exceeds its persistence bound');
55
+ }
56
+ return candidate;
38
57
  }
39
58
  /** Journals are constructed from JSON fields only; this is the persistence boundary. */
40
59
  export function journalJson(journal) {
60
+ if (jsonBytes(journal) > MAX_RUN_JOURNAL_BYTES)
61
+ throw new Error('Run step journal exceeds its persistence bound');
41
62
  return journal;
42
63
  }
64
+ function compactOutput(output) {
65
+ const serialized = json(output);
66
+ return {
67
+ $amalgmTruncated: true,
68
+ originalBytes: Buffer.byteLength(serialized),
69
+ jsonPreview: serialized.slice(0, MAX_OUTPUT_PREVIEW_CHARACTERS),
70
+ };
71
+ }
72
+ function removeStoredPreviews(journal) {
73
+ return {
74
+ version: 1,
75
+ steps: journal.steps.map((step) => {
76
+ if (!isCompactedOutput(step.output))
77
+ return step;
78
+ const { jsonPreview: _preview, ...output } = step.output;
79
+ return { ...step, output };
80
+ }),
81
+ };
82
+ }
83
+ function isCompactedOutput(value) {
84
+ if (!value || typeof value !== 'object' || Array.isArray(value))
85
+ return false;
86
+ const item = value;
87
+ return item.$amalgmTruncated === true && typeof item.jsonPreview === 'string';
88
+ }
89
+ function jsonBytes(value) {
90
+ return Buffer.byteLength(json(value));
91
+ }
92
+ function json(value) {
93
+ const serialized = JSON.stringify(value);
94
+ if (typeof serialized !== 'string')
95
+ throw new Error('Run step output must be JSON');
96
+ return serialized;
97
+ }
43
98
  function runStep(value) {
44
99
  if (!value || typeof value !== 'object' || Array.isArray(value))
45
100
  throw new Error('Run step is invalid');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amalgm/automations",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Amalgm's automation SDK: durable trigger admission and target-machine execution.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -0,0 +1,272 @@
1
+ -- Every delivery path admits the same declared workflow versions. The SDK owns
2
+ -- shape validation; this transaction-bound predicate prevents incomplete or
3
+ -- unsupported definitions from entering the durable execution ledger.
4
+
5
+ CREATE OR REPLACE FUNCTION public.amalgm_workflow_is_executable(p_compiled jsonb)
6
+ RETURNS boolean
7
+ LANGUAGE sql
8
+ IMMUTABLE
9
+ SET search_path = public
10
+ AS $$
11
+ SELECT jsonb_typeof(p_compiled) = 'object'
12
+ AND p_compiled->>'version' IN ('1', '2')
13
+ AND jsonb_typeof(p_compiled->'steps') = 'array'
14
+ AND jsonb_array_length(
15
+ CASE WHEN jsonb_typeof(p_compiled->'steps') = 'array'
16
+ THEN p_compiled->'steps' ELSE '[]'::jsonb END
17
+ ) BETWEEN 1 AND 100;
18
+ $$;
19
+
20
+ CREATE OR REPLACE FUNCTION public.list_amalgm_event_triggers(p_user_id uuid, p_target_id text)
21
+ RETURNS TABLE (
22
+ user_id uuid,
23
+ automation_id text,
24
+ trigger_id text,
25
+ target_id text,
26
+ source text,
27
+ event text,
28
+ secret text
29
+ )
30
+ LANGUAGE sql
31
+ SECURITY DEFINER
32
+ SET search_path = public
33
+ AS $$
34
+ SELECT t.user_id, t.automation_id, t.id, a.target_id, t.source, t.event, t.secret
35
+ FROM public.amalgm_automation_triggers t
36
+ JOIN public.amalgm_automations a
37
+ ON a.user_id = t.user_id AND a.id = t.automation_id
38
+ JOIN public.amalgm_automation_workflows w
39
+ ON w.user_id = a.user_id AND w.automation_id = a.id
40
+ WHERE t.kind = 'webhook' AND t.user_id = p_user_id
41
+ AND a.target_id = p_target_id AND t.enabled AND a.enabled
42
+ AND public.amalgm_workflow_is_executable(w.compiled);
43
+ $$;
44
+
45
+ CREATE OR REPLACE FUNCTION public.list_due_amalgm_crons(p_now timestamptz)
46
+ RETURNS TABLE (
47
+ user_id uuid,
48
+ automation_id text,
49
+ trigger_id text,
50
+ target_id text,
51
+ cron text,
52
+ timezone text,
53
+ next_run_at timestamptz,
54
+ remaining_occurrences integer
55
+ )
56
+ LANGUAGE sql
57
+ SECURITY DEFINER
58
+ SET search_path = public
59
+ AS $$
60
+ SELECT t.user_id, t.automation_id, t.id, a.target_id,
61
+ t.cron, t.timezone, t.next_run_at, t.remaining_occurrences
62
+ FROM public.amalgm_automation_triggers t
63
+ JOIN public.amalgm_automations a
64
+ ON a.user_id = t.user_id AND a.id = t.automation_id
65
+ JOIN public.amalgm_automation_workflows w
66
+ ON w.user_id = a.user_id AND w.automation_id = a.id
67
+ WHERE t.kind = 'schedule' AND t.next_run_at <= p_now
68
+ AND (t.remaining_occurrences IS NULL OR t.remaining_occurrences > 0)
69
+ AND t.enabled AND a.enabled
70
+ AND public.amalgm_workflow_is_executable(w.compiled)
71
+ ORDER BY t.next_run_at, t.user_id, t.automation_id, t.id;
72
+ $$;
73
+
74
+ CREATE OR REPLACE FUNCTION public.resolve_amalgm_webhook(p_endpoint_id text)
75
+ RETURNS TABLE (
76
+ user_id uuid,
77
+ automation_id text,
78
+ trigger_id text,
79
+ target_id text,
80
+ endpoint_id text,
81
+ source text,
82
+ event text,
83
+ secret text
84
+ )
85
+ LANGUAGE sql
86
+ SECURITY DEFINER
87
+ SET search_path = public
88
+ AS $$
89
+ SELECT t.user_id, t.automation_id, t.id, a.target_id,
90
+ t.endpoint_id, t.source, t.event, t.secret
91
+ FROM public.amalgm_automation_triggers t
92
+ JOIN public.amalgm_automations a
93
+ ON a.user_id = t.user_id AND a.id = t.automation_id
94
+ JOIN public.amalgm_automation_workflows w
95
+ ON w.user_id = a.user_id AND w.automation_id = a.id
96
+ WHERE t.kind = 'webhook' AND t.endpoint_id = p_endpoint_id
97
+ AND t.enabled AND a.enabled
98
+ AND public.amalgm_workflow_is_executable(w.compiled);
99
+ $$;
100
+
101
+ CREATE OR REPLACE FUNCTION public.enqueue_amalgm_run(
102
+ p_kind text,
103
+ p_user_id uuid,
104
+ p_automation_id text,
105
+ p_trigger_id text,
106
+ p_target_id text,
107
+ p_endpoint_id text,
108
+ p_delivery_key text,
109
+ p_expected_next_run_at timestamptz,
110
+ p_next_run_at timestamptz,
111
+ p_input jsonb,
112
+ p_now timestamptz
113
+ ) RETURNS SETOF public.amalgm_automation_runs
114
+ LANGUAGE plpgsql
115
+ SECURITY DEFINER
116
+ SET search_path = public
117
+ AS $$
118
+ DECLARE
119
+ v_inserted integer := 0;
120
+ BEGIN
121
+ IF p_kind = 'cron' THEN
122
+ IF p_endpoint_id IS NOT NULL OR p_delivery_key IS NOT NULL THEN
123
+ RAISE EXCEPTION 'Schedule admission cannot carry webhook identity';
124
+ END IF;
125
+ PERFORM 1
126
+ FROM public.amalgm_automation_triggers t
127
+ JOIN public.amalgm_automations a
128
+ ON a.user_id = t.user_id AND a.id = t.automation_id
129
+ WHERE t.user_id = p_user_id AND t.automation_id = p_automation_id
130
+ AND t.id = p_trigger_id AND a.target_id = p_target_id
131
+ AND t.kind = 'schedule' AND t.next_run_at = p_expected_next_run_at
132
+ AND (t.remaining_occurrences IS NULL OR t.remaining_occurrences > 0)
133
+ AND t.enabled AND a.enabled
134
+ FOR UPDATE OF t;
135
+ ELSIF p_kind = 'event' THEN
136
+ IF p_endpoint_id IS NULL THEN RAISE EXCEPTION 'Webhook endpoint is required'; END IF;
137
+ PERFORM 1
138
+ FROM public.amalgm_automation_triggers t
139
+ JOIN public.amalgm_automations a
140
+ ON a.user_id = t.user_id AND a.id = t.automation_id
141
+ WHERE t.user_id = p_user_id AND t.automation_id = p_automation_id
142
+ AND t.id = p_trigger_id AND a.target_id = p_target_id
143
+ AND t.kind = 'webhook' AND t.endpoint_id = p_endpoint_id
144
+ AND t.enabled AND a.enabled;
145
+ ELSE
146
+ RAISE EXCEPTION 'Invalid automation trigger kind';
147
+ END IF;
148
+ IF NOT FOUND THEN RETURN; END IF;
149
+
150
+ RETURN QUERY
151
+ INSERT INTO public.amalgm_automation_runs (
152
+ user_id, target_id, automation_id, trigger_id, workflow_id,
153
+ automation_payload, input, status, delivery_key, created_at
154
+ )
155
+ SELECT a.user_id, a.target_id, a.id, t.id, w.id,
156
+ jsonb_strip_nulls(jsonb_build_object(
157
+ 'id', a.id, 'targetId', a.target_id, 'name', a.name,
158
+ 'description', a.description, 'enabled', a.enabled,
159
+ 'trigger', jsonb_strip_nulls(jsonb_build_object(
160
+ 'id', t.id,
161
+ 'kind', CASE WHEN t.kind = 'schedule' THEN 'cron' ELSE 'event' END,
162
+ 'enabled', t.enabled, 'cron', t.cron, 'timezone', t.timezone,
163
+ 'source', t.source, 'event', t.event
164
+ )),
165
+ 'workflow', jsonb_strip_nulls(jsonb_build_object(
166
+ 'id', w.id, 'name', w.name, 'script', w.script,
167
+ 'compiled', w.compiled, 'allowlist', w.allowlist, 'limits', w.limits
168
+ ))
169
+ )),
170
+ p_input, 'pending', p_delivery_key, p_now
171
+ FROM public.amalgm_automations a
172
+ JOIN public.amalgm_automation_triggers t
173
+ ON t.user_id = a.user_id AND t.automation_id = a.id
174
+ JOIN public.amalgm_automation_workflows w
175
+ ON w.user_id = a.user_id AND w.automation_id = a.id
176
+ WHERE a.user_id = p_user_id AND a.id = p_automation_id AND t.id = p_trigger_id
177
+ AND a.target_id = p_target_id AND t.enabled AND a.enabled
178
+ AND (p_kind = 'cron' OR (t.kind = 'webhook' AND t.endpoint_id = p_endpoint_id))
179
+ AND public.amalgm_workflow_is_executable(w.compiled)
180
+ ON CONFLICT (user_id, automation_id, trigger_id, delivery_key)
181
+ WHERE delivery_key IS NOT NULL
182
+ DO UPDATE SET delivery_key = EXCLUDED.delivery_key
183
+ RETURNING *;
184
+ GET DIAGNOSTICS v_inserted = ROW_COUNT;
185
+
186
+ IF p_kind = 'cron' AND v_inserted = 1 THEN
187
+ UPDATE public.amalgm_automation_triggers SET
188
+ next_run_at = p_next_run_at,
189
+ remaining_occurrences = CASE WHEN remaining_occurrences IS NULL
190
+ THEN NULL ELSE remaining_occurrences - 1 END,
191
+ enabled = CASE WHEN remaining_occurrences = 1 THEN false ELSE enabled END,
192
+ updated_at = p_now
193
+ WHERE user_id = p_user_id AND automation_id = p_automation_id
194
+ AND id = p_trigger_id AND kind = 'schedule'
195
+ AND next_run_at = p_expected_next_run_at;
196
+ END IF;
197
+ END;
198
+ $$;
199
+
200
+ CREATE OR REPLACE FUNCTION public.enqueue_amalgm_manual_run(
201
+ p_user_id uuid,
202
+ p_automation_id text,
203
+ p_input jsonb,
204
+ p_idempotency_key text,
205
+ p_now timestamptz
206
+ ) RETURNS SETOF public.amalgm_automation_runs
207
+ LANGUAGE plpgsql
208
+ SECURITY DEFINER
209
+ SET search_path = public
210
+ AS $$
211
+ BEGIN
212
+ IF p_idempotency_key IS NOT NULL THEN
213
+ RETURN QUERY
214
+ SELECT r.*
215
+ FROM public.amalgm_automation_runs r
216
+ WHERE r.user_id = p_user_id
217
+ AND r.automation_id = p_automation_id
218
+ AND r.trigger_id = '@manual'
219
+ AND r.delivery_key = p_idempotency_key;
220
+ IF FOUND THEN RETURN; END IF;
221
+ END IF;
222
+
223
+ RETURN QUERY
224
+ INSERT INTO public.amalgm_automation_runs (
225
+ user_id, target_id, automation_id, trigger_id, workflow_id,
226
+ automation_payload, input, status, delivery_key, created_at
227
+ )
228
+ SELECT a.user_id, a.target_id, a.id, '@manual', w.id,
229
+ jsonb_strip_nulls(jsonb_build_object(
230
+ 'id', a.id, 'targetId', a.target_id, 'name', a.name,
231
+ 'description', a.description, 'enabled', a.enabled,
232
+ 'trigger', jsonb_build_object(
233
+ 'id', '@manual', 'kind', 'manual', 'enabled', true
234
+ ),
235
+ 'workflow', jsonb_strip_nulls(jsonb_build_object(
236
+ 'id', w.id, 'name', w.name, 'script', w.script,
237
+ 'compiled', w.compiled, 'allowlist', w.allowlist, 'limits', w.limits
238
+ ))
239
+ )),
240
+ p_input, 'pending', p_idempotency_key, p_now
241
+ FROM public.amalgm_automations a
242
+ JOIN public.amalgm_automation_workflows w
243
+ ON w.user_id = a.user_id AND w.automation_id = a.id
244
+ WHERE a.user_id = p_user_id AND a.id = p_automation_id AND a.enabled
245
+ AND public.amalgm_workflow_is_executable(w.compiled)
246
+ ON CONFLICT (user_id, automation_id, trigger_id, delivery_key)
247
+ WHERE delivery_key IS NOT NULL
248
+ DO UPDATE SET delivery_key = EXCLUDED.delivery_key
249
+ RETURNING *;
250
+ END;
251
+ $$;
252
+
253
+ REVOKE ALL ON FUNCTION public.amalgm_workflow_is_executable(jsonb) FROM PUBLIC;
254
+ REVOKE ALL ON FUNCTION public.list_amalgm_event_triggers(uuid, text) FROM PUBLIC;
255
+ REVOKE ALL ON FUNCTION public.list_due_amalgm_crons(timestamptz) FROM PUBLIC;
256
+ REVOKE ALL ON FUNCTION public.resolve_amalgm_webhook(text) FROM PUBLIC;
257
+ REVOKE ALL ON FUNCTION public.enqueue_amalgm_run(
258
+ text, uuid, text, text, text, text, text, timestamptz, timestamptz, jsonb, timestamptz
259
+ ) FROM PUBLIC;
260
+ REVOKE ALL ON FUNCTION public.enqueue_amalgm_manual_run(
261
+ uuid, text, jsonb, text, timestamptz
262
+ ) FROM PUBLIC;
263
+
264
+ GRANT EXECUTE ON FUNCTION public.list_amalgm_event_triggers(uuid, text) TO service_role;
265
+ GRANT EXECUTE ON FUNCTION public.list_due_amalgm_crons(timestamptz) TO service_role;
266
+ GRANT EXECUTE ON FUNCTION public.resolve_amalgm_webhook(text) TO service_role;
267
+ GRANT EXECUTE ON FUNCTION public.enqueue_amalgm_run(
268
+ text, uuid, text, text, text, text, text, timestamptz, timestamptz, jsonb, timestamptz
269
+ ) TO service_role;
270
+ GRANT EXECUTE ON FUNCTION public.enqueue_amalgm_manual_run(
271
+ uuid, text, jsonb, text, timestamptz
272
+ ) TO service_role;