@amalgm/automations 0.2.1 → 0.2.3
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 +39 -17
- package/PURPOSE.md +26 -11
- package/README.md +33 -4
- package/dist/host/config.d.ts +2 -0
- package/dist/host/config.js +8 -0
- package/dist/host/main.js +10 -5
- package/dist/host/server.d.ts +2 -0
- package/dist/host/server.js +25 -7
- package/dist/src/automations.d.ts +15 -9
- package/dist/src/automations.js +33 -76
- package/dist/src/cli.d.ts +1 -1
- package/dist/src/cli.js +5 -2
- package/dist/src/client.d.ts +1 -0
- package/dist/src/client.js +4 -2
- package/dist/src/contract.d.ts +11 -31
- package/dist/src/crud/context.d.ts +3 -1
- package/dist/src/crud/context.js +2 -1
- package/dist/src/crud/repository.d.ts +17 -6
- package/dist/src/crud/runs.d.ts +1 -1
- package/dist/src/crud/runs.js +24 -1
- package/dist/src/crud/triggers.js +18 -9
- package/dist/src/crud.d.ts +4 -2
- package/dist/src/crud.js +5 -2
- package/dist/src/events-http.d.ts +3 -5
- package/dist/src/events-http.js +22 -26
- package/dist/src/executor.d.ts +12 -3
- package/dist/src/executor.js +163 -44
- package/dist/src/http.js +8 -1
- package/dist/src/index.d.ts +6 -4
- package/dist/src/index.js +6 -4
- package/dist/src/machine-client.d.ts +1 -0
- package/dist/src/machine-client.js +2 -0
- package/dist/src/machine.d.ts +2 -1
- package/dist/src/machine.js +6 -1
- package/dist/src/mcp.d.ts +3 -0
- package/dist/src/mcp.js +61 -50
- package/dist/src/plan.d.ts +3 -0
- package/dist/src/plan.js +48 -0
- package/dist/src/run-contract.d.ts +52 -0
- package/dist/src/run-contract.js +1 -0
- package/dist/src/run-journal.d.ts +8 -0
- package/dist/src/run-journal.js +56 -0
- package/dist/src/runner.d.ts +14 -0
- package/dist/src/runner.js +70 -0
- package/dist/src/schema.d.ts +31 -15
- package/dist/src/schema.js +27 -4
- package/dist/src/supabase-crud/mappers.d.ts +3 -2
- package/dist/src/supabase-crud/mappers.js +5 -0
- package/dist/src/supabase-crud/rows.d.ts +3 -0
- package/dist/src/supabase-crud/workflow-runs.d.ts +1 -1
- package/dist/src/supabase-crud/workflow-runs.js +10 -0
- package/dist/src/supabase-machine.js +2 -0
- package/dist/src/supabase-store.d.ts +3 -6
- package/dist/src/supabase-store.js +13 -34
- package/dist/src/tool-surface.d.ts +47 -0
- package/dist/src/tool-surface.js +126 -0
- package/dist/src/types.d.ts +8 -24
- package/dist/src/webhook.d.ts +15 -1
- package/dist/src/webhook.js +86 -0
- package/package.json +3 -3
- package/skills/automations/SKILL.md +29 -16
- package/supabase/migrations/20260830010000_durable_step_retries.sql +332 -0
- package/supabase/migrations/20260831010000_public_webhook_endpoints.sql +287 -0
- package/supabase/migrations/20260831020000_manual_run_admission.sql +68 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { NotFoundError, ValidationError } from './errors.js';
|
|
2
|
+
export function createAutomationToolSurface(sdk) {
|
|
3
|
+
const view = async (automationId, runs = false) => {
|
|
4
|
+
const automation = await sdk.automations.get(automationId);
|
|
5
|
+
if (!automation)
|
|
6
|
+
throw new NotFoundError('Automation');
|
|
7
|
+
const [triggers, workflow, history] = await Promise.all([
|
|
8
|
+
sdk.triggers.list(automationId),
|
|
9
|
+
sdk.workflow.get(automationId),
|
|
10
|
+
runs === false ? undefined : sdk.runs.list(automationId, runs),
|
|
11
|
+
]);
|
|
12
|
+
return { automation, triggers, workflow, ...(history ? { runs: history } : {}) };
|
|
13
|
+
};
|
|
14
|
+
const surface = {
|
|
15
|
+
async create(input) {
|
|
16
|
+
const { schedules = [], webhooks = [], workflow, ...automationInput } = input;
|
|
17
|
+
validateCreateIds(schedules, webhooks);
|
|
18
|
+
const staged = schedules.length > 0 || webhooks.length > 0 || workflow !== undefined;
|
|
19
|
+
const requestedEnabled = automationInput.enabled !== false;
|
|
20
|
+
const automation = await sdk.automations.create({
|
|
21
|
+
...automationInput,
|
|
22
|
+
...(staged ? { enabled: false } : {}),
|
|
23
|
+
});
|
|
24
|
+
try {
|
|
25
|
+
if (workflow)
|
|
26
|
+
await sdk.workflow.create(automation.id, workflow);
|
|
27
|
+
for (const schedule of schedules)
|
|
28
|
+
await sdk.triggers.schedule.create(automation.id, schedule);
|
|
29
|
+
for (const webhook of webhooks)
|
|
30
|
+
await sdk.triggers.webhook.create(automation.id, webhook);
|
|
31
|
+
if (staged && requestedEnabled)
|
|
32
|
+
await sdk.automations.update(automation.id, { enabled: true });
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
throw disabledDraftError(automation.id, error);
|
|
36
|
+
}
|
|
37
|
+
return view(automation.id);
|
|
38
|
+
},
|
|
39
|
+
list: (query = {}) => sdk.automations.list(query),
|
|
40
|
+
get: view,
|
|
41
|
+
async update(automationId, input) {
|
|
42
|
+
requireChanges(input);
|
|
43
|
+
validateChanges(input.schedules, 'schedule');
|
|
44
|
+
validateChanges(input.webhooks, 'webhook');
|
|
45
|
+
const current = await sdk.automations.get(automationId);
|
|
46
|
+
if (!current)
|
|
47
|
+
throw new NotFoundError('Automation');
|
|
48
|
+
const changesResources = hasChanges(input.schedules) || hasChanges(input.webhooks) || Boolean(input.workflow);
|
|
49
|
+
const finalEnabled = input.patch?.enabled ?? current.enabled;
|
|
50
|
+
const { enabled: _enabled, ...metadata } = input.patch ?? {};
|
|
51
|
+
if (!changesResources) {
|
|
52
|
+
await sdk.automations.update(automationId, input.patch);
|
|
53
|
+
return view(automationId);
|
|
54
|
+
}
|
|
55
|
+
if (current.enabled)
|
|
56
|
+
await sdk.automations.update(automationId, { enabled: false });
|
|
57
|
+
try {
|
|
58
|
+
await applyTriggerChanges(automationId, input.schedules, sdk.triggers.schedule);
|
|
59
|
+
await applyTriggerChanges(automationId, input.webhooks, sdk.triggers.webhook);
|
|
60
|
+
await applyWorkflowChange(automationId, input.workflow, sdk.workflow);
|
|
61
|
+
await sdk.automations.update(automationId, { ...metadata, enabled: finalEnabled });
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
throw disabledDraftError(automationId, error);
|
|
65
|
+
}
|
|
66
|
+
return view(automationId);
|
|
67
|
+
},
|
|
68
|
+
async delete(automationId) {
|
|
69
|
+
await sdk.automations.delete(automationId);
|
|
70
|
+
return { deleted: automationId };
|
|
71
|
+
},
|
|
72
|
+
runNow: (automationId, input = {}) => sdk.runs.runNow(automationId, input),
|
|
73
|
+
};
|
|
74
|
+
return Object.freeze(surface);
|
|
75
|
+
}
|
|
76
|
+
async function applyTriggerChanges(automationId, changes, operations) {
|
|
77
|
+
if (!changes)
|
|
78
|
+
return;
|
|
79
|
+
for (const triggerId of changes.delete ?? [])
|
|
80
|
+
await operations.delete(automationId, triggerId);
|
|
81
|
+
for (const item of changes.update ?? [])
|
|
82
|
+
await operations.update(automationId, item.id, item.patch);
|
|
83
|
+
for (const input of changes.create ?? [])
|
|
84
|
+
await operations.create(automationId, input);
|
|
85
|
+
}
|
|
86
|
+
async function applyWorkflowChange(automationId, change, workflow) {
|
|
87
|
+
if (!change)
|
|
88
|
+
return;
|
|
89
|
+
if ('create' in change)
|
|
90
|
+
await workflow.create(automationId, change.create);
|
|
91
|
+
else if ('update' in change)
|
|
92
|
+
await workflow.update(automationId, change.update);
|
|
93
|
+
else
|
|
94
|
+
await workflow.delete(automationId);
|
|
95
|
+
}
|
|
96
|
+
function requireChanges(input) {
|
|
97
|
+
if (!input.patch && !hasChanges(input.schedules) && !hasChanges(input.webhooks) && !input.workflow) {
|
|
98
|
+
throw new ValidationError('Automation update must include a change');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function hasChanges(changes) {
|
|
102
|
+
return Boolean(changes && [changes.create, changes.update, changes.delete]
|
|
103
|
+
.some((items) => items && items.length > 0));
|
|
104
|
+
}
|
|
105
|
+
function validateChanges(changes, label) {
|
|
106
|
+
if (!changes)
|
|
107
|
+
return;
|
|
108
|
+
const ids = [
|
|
109
|
+
...changes.create?.flatMap(({ id }) => id ? [id] : []) ?? [],
|
|
110
|
+
...changes.update?.map(({ id }) => id) ?? [],
|
|
111
|
+
...changes.delete ?? [],
|
|
112
|
+
];
|
|
113
|
+
if (new Set(ids).size !== ids.length) {
|
|
114
|
+
throw new ValidationError(`The same ${label} trigger cannot be changed twice`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function validateCreateIds(schedules, webhooks) {
|
|
118
|
+
const ids = [...schedules, ...webhooks].flatMap(({ id }) => id ? [id] : []);
|
|
119
|
+
if (new Set(ids).size !== ids.length) {
|
|
120
|
+
throw new ValidationError('Trigger ids must be unique within an automation');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function disabledDraftError(automationId, error) {
|
|
124
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
125
|
+
return new Error(`Automation ${automationId} remains disabled because configuration failed: ${reason}`);
|
|
126
|
+
}
|
package/dist/src/types.d.ts
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import type { Json } from './contract.js';
|
|
2
2
|
export type { Json } from './contract.js';
|
|
3
|
-
/** The only authenticated identity Automations accepts from Core. */
|
|
4
|
-
export interface AutomationTarget {
|
|
5
|
-
userId: string;
|
|
6
|
-
targetId: string;
|
|
7
|
-
}
|
|
8
3
|
interface StoredTriggerBase {
|
|
9
4
|
userId: string;
|
|
10
5
|
automationId: string;
|
|
@@ -13,9 +8,10 @@ interface StoredTriggerBase {
|
|
|
13
8
|
}
|
|
14
9
|
export interface StoredEventTrigger extends StoredTriggerBase {
|
|
15
10
|
kind: 'event';
|
|
11
|
+
endpointId: string;
|
|
16
12
|
source: string;
|
|
17
13
|
event: string;
|
|
18
|
-
secret: string;
|
|
14
|
+
secret: string | null;
|
|
19
15
|
}
|
|
20
16
|
export interface DueCronTrigger extends StoredTriggerBase {
|
|
21
17
|
kind: 'cron';
|
|
@@ -32,6 +28,9 @@ export type RunInput = {
|
|
|
32
28
|
} | {
|
|
33
29
|
kind: 'cron';
|
|
34
30
|
scheduledFor: string;
|
|
31
|
+
} | {
|
|
32
|
+
kind: 'manual';
|
|
33
|
+
payload: Json;
|
|
35
34
|
};
|
|
36
35
|
export type RunStatus = 'pending' | 'sent' | 'running' | 'completed' | 'failed';
|
|
37
36
|
export interface AutomationRun {
|
|
@@ -51,27 +50,12 @@ export interface AutomationRun {
|
|
|
51
50
|
output?: Json;
|
|
52
51
|
error?: string;
|
|
53
52
|
}
|
|
54
|
-
export interface RunUpdate {
|
|
55
|
-
status: 'running' | 'completed' | 'failed';
|
|
56
|
-
startedAt?: string;
|
|
57
|
-
finishedAt?: string;
|
|
58
|
-
output?: Json;
|
|
59
|
-
error?: string;
|
|
60
|
-
}
|
|
61
53
|
export interface AutomationStore {
|
|
62
|
-
|
|
54
|
+
eventTrigger(endpointId: string): Promise<StoredEventTrigger | null>;
|
|
63
55
|
dueCronTriggers(now: Date): Promise<DueCronTrigger[]>;
|
|
64
56
|
enqueueEvent(trigger: StoredEventTrigger, input: Extract<RunInput, {
|
|
65
57
|
kind: 'event';
|
|
66
|
-
}>, now: Date): Promise<AutomationRun | null>;
|
|
58
|
+
}>, now: Date, deliveryKey: string | null): Promise<AutomationRun | null>;
|
|
67
59
|
enqueueCron(trigger: DueCronTrigger, nextRunAt: string, now: Date): Promise<AutomationRun | null>;
|
|
68
|
-
pendingRuns(target: AutomationTarget): Promise<AutomationRun[]>;
|
|
69
|
-
markSent(runId: string, target: AutomationTarget, now: Date): Promise<boolean>;
|
|
70
|
-
updateRun(target: AutomationTarget, runId: string, update: RunUpdate): Promise<AutomationRun | null>;
|
|
71
|
-
}
|
|
72
|
-
/** The complete transport capability Automations needs from Core. */
|
|
73
|
-
export interface AutomationTransport {
|
|
74
|
-
isOnline(target: AutomationTarget): boolean;
|
|
75
|
-
send(run: AutomationRun): Promise<boolean>;
|
|
76
60
|
}
|
|
77
|
-
export type AutomationLog = (event: 'event.rejected' | 'run.pending'
|
|
61
|
+
export type AutomationLog = (event: 'event.rejected' | 'run.pending', details: Readonly<Record<string, string>>) => void;
|
package/dist/src/webhook.d.ts
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import type { Json, StoredEventTrigger } from './types.js';
|
|
2
2
|
export declare function normalizeHeaders(headers: Record<string, string>): Record<string, string>;
|
|
3
|
-
export declare function verifyEventSecret(secret: string, headers: Record<string, string>, body: Buffer): boolean;
|
|
3
|
+
export declare function verifyEventSecret(secret: string | null, headers: Record<string, string>, body: Buffer): boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Generates and verifies the bearer capability embedded in a webhook URL.
|
|
6
|
+
* Supabase stores only the random endpoint id; possession also requires the
|
|
7
|
+
* service-held HMAC key, so a database read cannot mint working URLs.
|
|
8
|
+
*/
|
|
9
|
+
export declare class WebhookEndpoints {
|
|
10
|
+
#private;
|
|
11
|
+
readonly origin: string;
|
|
12
|
+
constructor(origin: string, key: string | Buffer, randomId?: () => string);
|
|
13
|
+
createId(): string;
|
|
14
|
+
url(id: string): string;
|
|
15
|
+
resolve(token: string): string | null;
|
|
16
|
+
}
|
|
17
|
+
export declare function webhookDeliveryKey(headers: Record<string, string>, payload: Json): string | null;
|
|
4
18
|
export declare function matchesEvent(trigger: Pick<StoredEventTrigger, 'source' | 'event'>, source: string, event: string): boolean;
|
|
5
19
|
export declare function eventReferences(headers: Record<string, string>, payload: Json): {
|
|
6
20
|
primary: {
|
package/dist/src/webhook.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
const HMAC_HEADERS = ['x-webhook-signature', 'x-hub-signature-256', 'x-hub-signature'];
|
|
3
3
|
const TOKEN_HEADERS = ['x-amalgm-webhook-secret', 'x-webhook-secret', 'x-gitlab-token'];
|
|
4
|
+
const DELIVERY_HEADERS = [
|
|
5
|
+
'idempotency-key',
|
|
6
|
+
'x-github-delivery',
|
|
7
|
+
'x-gitlab-event-uuid',
|
|
8
|
+
'x-linear-delivery',
|
|
9
|
+
'svix-id',
|
|
10
|
+
'webhook-id',
|
|
11
|
+
];
|
|
12
|
+
const ENDPOINT_ID = /^[a-f0-9]{32}$/;
|
|
13
|
+
const MAX_DELIVERY_KEY_BYTES = 500;
|
|
4
14
|
function equal(left, right) {
|
|
5
15
|
const a = Buffer.from(left);
|
|
6
16
|
const b = Buffer.from(right);
|
|
@@ -10,6 +20,8 @@ export function normalizeHeaders(headers) {
|
|
|
10
20
|
return Object.fromEntries(Object.entries(headers).map(([name, value]) => [name.toLowerCase(), value]));
|
|
11
21
|
}
|
|
12
22
|
export function verifyEventSecret(secret, headers, body) {
|
|
23
|
+
if (secret === null)
|
|
24
|
+
return true;
|
|
13
25
|
const normalized = normalizeHeaders(headers);
|
|
14
26
|
for (const name of HMAC_HEADERS) {
|
|
15
27
|
const supplied = normalized[name];
|
|
@@ -28,6 +40,63 @@ export function verifyEventSecret(secret, headers, body) {
|
|
|
28
40
|
}
|
|
29
41
|
return false;
|
|
30
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Generates and verifies the bearer capability embedded in a webhook URL.
|
|
45
|
+
* Supabase stores only the random endpoint id; possession also requires the
|
|
46
|
+
* service-held HMAC key, so a database read cannot mint working URLs.
|
|
47
|
+
*/
|
|
48
|
+
export class WebhookEndpoints {
|
|
49
|
+
origin;
|
|
50
|
+
#key;
|
|
51
|
+
#randomId;
|
|
52
|
+
constructor(origin, key, randomId = endpointId) {
|
|
53
|
+
this.origin = webhookOrigin(origin);
|
|
54
|
+
this.#key = Buffer.isBuffer(key) ? Buffer.from(key) : Buffer.from(key, 'utf8');
|
|
55
|
+
if (this.#key.length < 32)
|
|
56
|
+
throw new Error('Webhook endpoint key must contain at least 32 bytes');
|
|
57
|
+
this.#randomId = randomId;
|
|
58
|
+
}
|
|
59
|
+
createId() {
|
|
60
|
+
const value = this.#randomId();
|
|
61
|
+
if (!ENDPOINT_ID.test(value))
|
|
62
|
+
throw new Error('Webhook endpoint id must be 128-bit lowercase hex');
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
url(id) {
|
|
66
|
+
if (!ENDPOINT_ID.test(id))
|
|
67
|
+
throw new Error('Invalid webhook endpoint id');
|
|
68
|
+
return `${this.origin}/e/${id}.${this.#signature(id)}`;
|
|
69
|
+
}
|
|
70
|
+
resolve(token) {
|
|
71
|
+
const separator = token.indexOf('.');
|
|
72
|
+
if (separator === -1 || token.indexOf('.', separator + 1) !== -1)
|
|
73
|
+
return null;
|
|
74
|
+
const id = token.slice(0, separator);
|
|
75
|
+
const supplied = token.slice(separator + 1);
|
|
76
|
+
if (!ENDPOINT_ID.test(id) || !supplied || !equal(supplied, this.#signature(id)))
|
|
77
|
+
return null;
|
|
78
|
+
return id;
|
|
79
|
+
}
|
|
80
|
+
#signature(id) {
|
|
81
|
+
return crypto.createHmac('sha256', this.#key).update(id).digest('base64url');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export function webhookDeliveryKey(headers, payload) {
|
|
85
|
+
const normalized = normalizeHeaders(headers);
|
|
86
|
+
for (const name of DELIVERY_HEADERS) {
|
|
87
|
+
const value = normalized[name]?.trim();
|
|
88
|
+
if (!value)
|
|
89
|
+
continue;
|
|
90
|
+
return boundedDeliveryKey(`${name}:${value}`);
|
|
91
|
+
}
|
|
92
|
+
if (normalized['stripe-signature']) {
|
|
93
|
+
const object = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {};
|
|
94
|
+
if (typeof object.id === 'string' && object.id.trim()) {
|
|
95
|
+
return boundedDeliveryKey(`stripe-event:${object.id.trim()}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
31
100
|
export function matchesEvent(trigger, source, event) {
|
|
32
101
|
return (trigger.source === '*' || trigger.source === source)
|
|
33
102
|
&& (trigger.event === '*' || trigger.event === event);
|
|
@@ -58,3 +127,20 @@ export function eventReferences(headers, payload) {
|
|
|
58
127
|
export function eventReference(headers, payload) {
|
|
59
128
|
return eventReferences(headers, payload).primary;
|
|
60
129
|
}
|
|
130
|
+
const endpointId = () => crypto.randomBytes(16).toString('hex');
|
|
131
|
+
function webhookOrigin(value) {
|
|
132
|
+
const parsed = new URL(value);
|
|
133
|
+
const local = parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1' || parsed.hostname.endsWith('.local');
|
|
134
|
+
if (parsed.protocol !== 'https:' && !(local && parsed.protocol === 'http:')) {
|
|
135
|
+
throw new Error('Webhook origin must use HTTPS');
|
|
136
|
+
}
|
|
137
|
+
if (parsed.pathname !== '/' || parsed.search || parsed.hash)
|
|
138
|
+
throw new Error('Webhook origin cannot include a path');
|
|
139
|
+
return parsed.toString().replace(/\/$/, '');
|
|
140
|
+
}
|
|
141
|
+
function boundedDeliveryKey(value) {
|
|
142
|
+
if (Buffer.byteLength(value) > MAX_DELIVERY_KEY_BYTES) {
|
|
143
|
+
throw Object.assign(new Error('Webhook delivery id is too long'), { status: 400 });
|
|
144
|
+
}
|
|
145
|
+
return value;
|
|
146
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amalgm/automations",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Amalgm's
|
|
3
|
+
"version": "0.2.3",
|
|
4
|
+
"description": "Amalgm's automation SDK: durable trigger admission and target-machine execution.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"files": [
|
|
40
40
|
"dist",
|
|
41
41
|
"skills",
|
|
42
|
-
"supabase",
|
|
42
|
+
"supabase/migrations",
|
|
43
43
|
"AXIOMS.md",
|
|
44
44
|
"PURPOSE.md",
|
|
45
45
|
"README.md"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: automations
|
|
3
|
-
description: Create, inspect, change, or
|
|
3
|
+
description: Create, inspect, change, delete, or manually run complete Amalgm automation definitions and inspect their run history through the Automations MCP tools.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Amalgm Automations
|
|
@@ -11,11 +11,8 @@ the agent adapter over the same hosted SDK used by the UI.
|
|
|
11
11
|
## Create a scheduled notification
|
|
12
12
|
|
|
13
13
|
For a request such as “remind me to call my mom every minute for the next ten
|
|
14
|
-
minutes,”
|
|
15
|
-
|
|
16
|
-
1. `amalgm_automations_create` with a clear name and the current machine target.
|
|
17
|
-
2. `amalgm_workflow_create` with a readable script summary and this compiled
|
|
18
|
-
plan:
|
|
14
|
+
minutes,” make one `amalgm_automations_create` call containing the complete
|
|
15
|
+
definition. Include a readable workflow summary and this compiled plan:
|
|
19
16
|
|
|
20
17
|
```json
|
|
21
18
|
{
|
|
@@ -30,23 +27,39 @@ minutes,” create three resources for the current machine:
|
|
|
30
27
|
}
|
|
31
28
|
```
|
|
32
29
|
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
Include one schedule with cron `* * * * *`, the user's timezone, and
|
|
31
|
+
`maxOccurrences: 10`. Omit `targetId` in a machine-bound session; never guess
|
|
32
|
+
an opaque target id.
|
|
35
33
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
The tool stages multi-resource configuration disabled and enables it only after
|
|
35
|
+
setup succeeds. If setup fails, report the returned disabled draft id. Never
|
|
36
|
+
replace a bounded occurrence count with an unbounded schedule plus a promise to
|
|
37
|
+
clean it up later.
|
|
40
38
|
|
|
41
39
|
## Read and change
|
|
42
40
|
|
|
43
|
-
Use
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
Use `amalgm_automations_list` or `amalgm_automations_get` before changing an
|
|
42
|
+
existing automation. Use `amalgm_automations_update` for grouped metadata,
|
|
43
|
+
schedule, webhook, or workflow changes, and `amalgm_automations_delete` only
|
|
44
|
+
after identifying the exact automation. Schedule and webhook triggers remain
|
|
45
|
+
distinct resources inside the definition. A workflow is zero-or-one per
|
|
46
|
+
automation. Run history is read-only, requested through `get`, and remains
|
|
47
|
+
after configuration deletion.
|
|
48
|
+
|
|
49
|
+
Use `amalgm_automations_run_now` when the user wants an enabled automation to
|
|
50
|
+
run immediately. Supply `idempotency_key` when a caller may retry the same
|
|
51
|
+
request. The result is a durable `pending` run, not proof that its selected
|
|
52
|
+
machine has finished it; use `amalgm_automations_get` with run history when the
|
|
53
|
+
user asks for the outcome.
|
|
54
|
+
|
|
55
|
+
Action discovery belongs to the Tools product, not Automations.
|
|
46
56
|
|
|
47
57
|
`pending` means a run is durable and waiting for its selected machine. `sent`
|
|
48
58
|
or `running` means that machine holds a lease. `completed` and `failed` are
|
|
49
59
|
terminal. Do not infer delivery from the schedule alone; inspect runs when the
|
|
50
60
|
user asks whether it actually happened.
|
|
51
61
|
|
|
52
|
-
|
|
62
|
+
Each webhook trigger returns one copyable `webhookUrl`; use that URL as the
|
|
63
|
+
provider destination. A separate provider signing secret is optional and
|
|
64
|
+
write-only. Never echo, log, or place either credential in a workflow. Rotate
|
|
65
|
+
the URL when its bearer capability may have been exposed.
|