@desplega.ai/agent-swarm 1.87.0 → 1.89.0
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/README.md +5 -1
- package/openapi.json +53 -1
- package/package.json +6 -5
- package/plugin/skills/composio/SKILL.md +98 -0
- package/src/be/db.ts +374 -9
- package/src/be/migrations/080_skill_system_defaults.sql +8 -0
- package/src/be/migrations/081_metrics.sql +39 -0
- package/src/be/migrations/082_user_audit_fields.sql +120 -0
- package/src/be/modelsdev-cache.json +3825 -2417
- package/src/be/seed/registry.ts +3 -2
- package/src/be/seed-skills/index.ts +179 -0
- package/src/cli.tsx +51 -4
- package/src/commands/e2b-stack-wizard.tsx +394 -0
- package/src/commands/e2b.ts +1352 -53
- package/src/commands/onboard/dashboard-url.ts +29 -0
- package/src/commands/onboard/steps/post-dashboard.tsx +3 -1
- package/src/commands/onboard.tsx +3 -1
- package/src/commands/runner.ts +154 -22
- package/src/commands/x.ts +118 -0
- package/src/e2b/dispatch.ts +234 -18
- package/src/github/handlers.ts +40 -1
- package/src/heartbeat/heartbeat.ts +26 -5
- package/src/http/active-sessions.ts +32 -1
- package/src/http/auth.ts +36 -0
- package/src/http/core.ts +20 -16
- package/src/http/db-query.ts +20 -0
- package/src/http/index.ts +2 -0
- package/src/http/memory.ts +13 -1
- package/src/http/metrics.ts +447 -0
- package/src/http/operator-actor.ts +9 -0
- package/src/http/poll.ts +11 -1
- package/src/http/skills.ts +53 -0
- package/src/http/tasks.ts +4 -1
- package/src/http/webhooks.ts +75 -0
- package/src/http/workflows.ts +5 -1
- package/src/integrations/kapso/client.ts +82 -0
- package/src/memory/automatic-task-gate.ts +47 -0
- package/src/metrics/version.ts +26 -0
- package/src/prompts/base-prompt.ts +24 -1
- package/src/prompts/session-templates.ts +74 -0
- package/src/providers/claude-adapter.ts +19 -0
- package/src/providers/codex-adapter.ts +22 -0
- package/src/providers/ctx-mode-env.ts +10 -0
- package/src/providers/opencode-adapter.ts +72 -7
- package/src/server.ts +10 -1
- package/src/slack/blocks.ts +12 -4
- package/src/slack/watcher.ts +3 -3
- package/src/telemetry.ts +14 -1
- package/src/templates.d.ts +4 -0
- package/src/tests/base-prompt.test.ts +76 -0
- package/src/tests/budget-claim-gate.test.ts +26 -0
- package/src/tests/claude-adapter.test.ts +86 -1
- package/src/tests/codex-adapter.test.ts +89 -0
- package/src/tests/core-auth.test.ts +8 -1
- package/src/tests/e2b-dispatch.test.ts +603 -11
- package/src/tests/events-http.test.ts +6 -2
- package/src/tests/github-handlers-cancel-config.test.ts +262 -0
- package/src/tests/heartbeat.test.ts +84 -3
- package/src/tests/http-api-integration.test.ts +116 -1
- package/src/tests/kapso-client.test.ts +74 -1
- package/src/tests/kapso-inbound.test.ts +60 -2
- package/src/tests/metrics-http.test.ts +247 -0
- package/src/tests/opencode-adapter.test.ts +185 -30
- package/src/tests/prompt-template-session.test.ts +4 -2
- package/src/tests/runner-repo-autostash.test.ts +117 -0
- package/src/tests/runner-requester-profile.test.ts +25 -0
- package/src/tests/runner-skills-refresh.test.ts +1 -1
- package/src/tests/self-improvement.test.ts +89 -0
- package/src/tests/skill-update-scope.test.ts +88 -1
- package/src/tests/slack-blocks.test.ts +15 -0
- package/src/tests/swarm-x-tool.test.ts +90 -0
- package/src/tests/system-default-skills.test.ts +122 -0
- package/src/tests/telemetry-init.test.ts +86 -0
- package/src/tests/ui-logs-parser.test.ts +271 -0
- package/src/tests/user-token-rest-auth.test.ts +129 -0
- package/src/tests/workflow-async-v2.test.ts +23 -0
- package/src/tests/x-composio.test.ts +122 -0
- package/src/tools/create-metric.ts +191 -0
- package/src/tools/skills/skill-delete.ts +14 -0
- package/src/tools/skills/skill-update.ts +14 -0
- package/src/tools/store-progress.ts +19 -5
- package/src/tools/swarm-x.ts +116 -0
- package/src/tools/tool-config.ts +6 -0
- package/src/types.ts +121 -0
- package/src/utils/request-auth-context.ts +28 -0
- package/src/utils/skills-refresh.ts +2 -2
- package/src/workflows/engine.ts +24 -2
- package/src/workflows/executors/agent-task.ts +2 -0
- package/src/x/composio.ts +295 -0
- package/templates/skills/artifacts/config.json +1 -0
- package/templates/skills/attio-interaction/SKILL.md +279 -0
- package/templates/skills/attio-interaction/config.json +14 -0
- package/templates/skills/attio-interaction/content.md +272 -0
- package/templates/skills/kv-storage/config.json +1 -0
- package/templates/skills/pages/config.json +1 -0
- package/templates/skills/scheduled-task-resilience/config.json +1 -0
- package/templates/skills/swarm-scripts/SKILL.md +91 -0
- package/templates/skills/swarm-scripts/config.json +14 -0
- package/templates/skills/swarm-scripts/content.md +86 -0
- package/templates/skills/workflow-iterate/config.json +1 -0
- package/templates/skills/workflow-structured-output/config.json +1 -0
- package/tsconfig.json +2 -1
package/src/workflows/engine.ts
CHANGED
|
@@ -25,6 +25,10 @@ const DEFAULT_TIMEOUT_MS = 30_000;
|
|
|
25
25
|
const MAX_ITERATIONS = Number(process.env.WORKFLOW_MAX_ITERATIONS) || 100;
|
|
26
26
|
const MAX_STEPS_PER_RUN = Number(process.env.WORKFLOW_MAX_STEPS_PER_RUN) || 500;
|
|
27
27
|
|
|
28
|
+
export interface WorkflowExecutionOptions {
|
|
29
|
+
requestedByUserId?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
28
32
|
/**
|
|
29
33
|
* Error thrown when trigger data fails validation against a workflow's triggerSchema.
|
|
30
34
|
*/
|
|
@@ -50,6 +54,7 @@ export async function startWorkflowExecution(
|
|
|
50
54
|
workflow: Workflow,
|
|
51
55
|
triggerData: unknown,
|
|
52
56
|
registry: ExecutorRegistry,
|
|
57
|
+
options: WorkflowExecutionOptions = {},
|
|
53
58
|
): Promise<string> {
|
|
54
59
|
// Validate trigger data against triggerSchema (before any DB writes)
|
|
55
60
|
if (workflow.triggerSchema) {
|
|
@@ -76,6 +81,9 @@ export async function startWorkflowExecution(
|
|
|
76
81
|
|
|
77
82
|
// Resolve inputs and merge into initial context
|
|
78
83
|
const ctx: Record<string, unknown> = { trigger: triggerData };
|
|
84
|
+
if (options.requestedByUserId) {
|
|
85
|
+
ctx.swarm = { requestedByUserId: options.requestedByUserId };
|
|
86
|
+
}
|
|
79
87
|
|
|
80
88
|
// Inject workflow-level metadata for interpolation ({{workflow.dir}}, {{workflow.vcsRepo}})
|
|
81
89
|
if (workflow.dir || workflow.vcsRepo) {
|
|
@@ -98,7 +106,16 @@ export async function startWorkflowExecution(
|
|
|
98
106
|
|
|
99
107
|
const entryNodes = findEntryNodes(workflow.definition);
|
|
100
108
|
const secretKeys = getSecretInputKeys(workflow.input);
|
|
101
|
-
await walkGraph(
|
|
109
|
+
await walkGraph(
|
|
110
|
+
workflow.definition,
|
|
111
|
+
runId,
|
|
112
|
+
ctx,
|
|
113
|
+
entryNodes,
|
|
114
|
+
registry,
|
|
115
|
+
workflow.id,
|
|
116
|
+
secretKeys,
|
|
117
|
+
options,
|
|
118
|
+
);
|
|
102
119
|
return runId;
|
|
103
120
|
}
|
|
104
121
|
|
|
@@ -127,6 +144,7 @@ export async function walkGraph(
|
|
|
127
144
|
registry: ExecutorRegistry,
|
|
128
145
|
workflowId?: string,
|
|
129
146
|
secretKeys: Set<string> = new Set(),
|
|
147
|
+
options: WorkflowExecutionOptions = {},
|
|
130
148
|
): Promise<void> {
|
|
131
149
|
let nodeExecutionCount = 0;
|
|
132
150
|
const completedNodeIds = new Set(getCompletedStepNodeIds(runId));
|
|
@@ -234,7 +252,7 @@ export async function walkGraph(
|
|
|
234
252
|
// Execute all pending nodes in parallel
|
|
235
253
|
const results = await Promise.all(
|
|
236
254
|
pendingNodes.map((node) =>
|
|
237
|
-
executeStep(def, runId, ctx, node, registry, workflowId, secretKeys).catch(
|
|
255
|
+
executeStep(def, runId, ctx, node, registry, workflowId, secretKeys, options).catch(
|
|
238
256
|
(_err): StepResult => ({
|
|
239
257
|
outcome: "failed",
|
|
240
258
|
successors: [],
|
|
@@ -385,6 +403,7 @@ async function executeStep(
|
|
|
385
403
|
registry: ExecutorRegistry,
|
|
386
404
|
workflowId?: string,
|
|
387
405
|
secretKeys: Set<string> = new Set(),
|
|
406
|
+
options: WorkflowExecutionOptions = {},
|
|
388
407
|
): Promise<StepResult> {
|
|
389
408
|
// Use iteration-aware idempotency key to support loops.
|
|
390
409
|
// Count existing steps for this node to determine the current iteration.
|
|
@@ -438,6 +457,7 @@ async function executeStep(
|
|
|
438
457
|
if (ctx.trigger !== undefined) interpolationCtx.trigger = ctx.trigger;
|
|
439
458
|
if (ctx.input !== undefined) interpolationCtx.input = ctx.input;
|
|
440
459
|
if (ctx.workflow !== undefined) interpolationCtx.workflow = ctx.workflow;
|
|
460
|
+
if (ctx.swarm !== undefined) interpolationCtx.swarm = ctx.swarm;
|
|
441
461
|
// Resolve declared inputs
|
|
442
462
|
for (const [localName, sourcePath] of Object.entries(node.inputs)) {
|
|
443
463
|
const keys = sourcePath.split(".");
|
|
@@ -457,6 +477,7 @@ async function executeStep(
|
|
|
457
477
|
if (ctx.trigger !== undefined) interpolationCtx.trigger = ctx.trigger;
|
|
458
478
|
if (ctx.input !== undefined) interpolationCtx.input = ctx.input;
|
|
459
479
|
if (ctx.workflow !== undefined) interpolationCtx.workflow = ctx.workflow;
|
|
480
|
+
if (ctx.swarm !== undefined) interpolationCtx.swarm = ctx.swarm;
|
|
460
481
|
}
|
|
461
482
|
|
|
462
483
|
// 3c. Validate resolved inputs against inputSchema if defined
|
|
@@ -492,6 +513,7 @@ async function executeStep(
|
|
|
492
513
|
nodeId: node.id,
|
|
493
514
|
workflowId: workflowId || "",
|
|
494
515
|
dryRun: false,
|
|
516
|
+
requestedByUserId: options.requestedByUserId,
|
|
495
517
|
};
|
|
496
518
|
|
|
497
519
|
const timeoutMs =
|
|
@@ -18,6 +18,7 @@ const AgentTaskConfigSchema = z.object({
|
|
|
18
18
|
vcsRepo: z.string().min(1).optional(),
|
|
19
19
|
model: z.string().min(1).optional(),
|
|
20
20
|
parentTaskId: z.string().uuid().optional(),
|
|
21
|
+
requestedByUserId: z.string().optional(),
|
|
21
22
|
outputSchema: z.record(z.string(), z.unknown()).optional(),
|
|
22
23
|
followUpConfig: FollowUpConfigSchema.optional(),
|
|
23
24
|
});
|
|
@@ -95,6 +96,7 @@ export class AgentTaskExecutor extends BaseExecutor<
|
|
|
95
96
|
vcsRepo: effectiveVcsRepo,
|
|
96
97
|
model: config.model,
|
|
97
98
|
parentTaskId: config.parentTaskId,
|
|
99
|
+
requestedByUserId: config.requestedByUserId ?? meta.requestedByUserId,
|
|
98
100
|
outputSchema: config.outputSchema,
|
|
99
101
|
followUpConfig: config.followUpConfig,
|
|
100
102
|
contextKey: workflowContextKey({ workflowRunId: meta.runId }),
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { registerVolatileSecret, scrubObject, scrubSecrets } from "../utils/secret-scrubber";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_COMPOSIO_BASE_URL = "https://backend.composio.dev/api/v3.1";
|
|
4
|
+
export const COMPOSIO_HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"] as const;
|
|
5
|
+
|
|
6
|
+
const HTTP_METHODS = new Set<string>(COMPOSIO_HTTP_METHODS);
|
|
7
|
+
|
|
8
|
+
export type ComposioHttpMethod = (typeof COMPOSIO_HTTP_METHODS)[number];
|
|
9
|
+
|
|
10
|
+
export interface ComposioRequestArgs {
|
|
11
|
+
baseUrl: string;
|
|
12
|
+
body?: unknown;
|
|
13
|
+
endpoint: string;
|
|
14
|
+
headers: Record<string, string>;
|
|
15
|
+
method: string;
|
|
16
|
+
query: Array<[string, string]>;
|
|
17
|
+
raw: boolean;
|
|
18
|
+
useOrgKey: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ComposioRequestDeps {
|
|
22
|
+
env?: Record<string, string | undefined>;
|
|
23
|
+
fetch?: typeof fetch;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ComposioExecutionResult {
|
|
27
|
+
body: unknown;
|
|
28
|
+
contentType: string | null;
|
|
29
|
+
error?: string;
|
|
30
|
+
formattedBody: string;
|
|
31
|
+
method: string;
|
|
32
|
+
ok: boolean;
|
|
33
|
+
status: number;
|
|
34
|
+
statusText: string;
|
|
35
|
+
text: string;
|
|
36
|
+
url: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function parseComposioArgs(
|
|
40
|
+
argv: string[],
|
|
41
|
+
env: Record<string, string | undefined> = process.env,
|
|
42
|
+
): ComposioRequestArgs {
|
|
43
|
+
const method = argv[0]?.toUpperCase();
|
|
44
|
+
if (!method || !HTTP_METHODS.has(method)) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
"first argument after composio must be an HTTP method: GET, POST, PUT, PATCH, DELETE, or HEAD",
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const endpoint = argv[1];
|
|
51
|
+
if (!endpoint || endpoint.startsWith("-")) {
|
|
52
|
+
throw new Error("endpoint path is required, e.g. /tools");
|
|
53
|
+
}
|
|
54
|
+
assertRelativeComposioPath(endpoint);
|
|
55
|
+
|
|
56
|
+
const parsed: ComposioRequestArgs = {
|
|
57
|
+
baseUrl: env.COMPOSIO_BASE_URL || DEFAULT_COMPOSIO_BASE_URL,
|
|
58
|
+
endpoint,
|
|
59
|
+
headers: {},
|
|
60
|
+
method,
|
|
61
|
+
query: [],
|
|
62
|
+
raw: false,
|
|
63
|
+
useOrgKey: false,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
for (let i = 2; i < argv.length; i++) {
|
|
67
|
+
const arg = argv[i] ?? "";
|
|
68
|
+
if (arg === "--base-url") {
|
|
69
|
+
parsed.baseUrl = requiredValue(argv, ++i, "--base-url");
|
|
70
|
+
} else if (arg === "--body" || arg === "--data") {
|
|
71
|
+
parsed.body = parseJsonArg(requiredValue(argv, ++i, arg), arg);
|
|
72
|
+
} else if (arg === "--query" || arg === "-q") {
|
|
73
|
+
parsed.query.push(parsePair(requiredValue(argv, ++i, arg), arg));
|
|
74
|
+
} else if (arg === "--header" || arg === "-H") {
|
|
75
|
+
const [key, value] = parseHeader(requiredValue(argv, ++i, arg));
|
|
76
|
+
parsed.headers[key] = value;
|
|
77
|
+
} else if (arg === "--org") {
|
|
78
|
+
parsed.useOrgKey = true;
|
|
79
|
+
} else if (arg === "--raw") {
|
|
80
|
+
parsed.raw = true;
|
|
81
|
+
} else {
|
|
82
|
+
throw new Error(`unknown option: ${arg}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if ((method === "GET" || method === "HEAD") && parsed.body !== undefined) {
|
|
87
|
+
throw new Error(`${method} requests cannot include --body`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return parsed;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function executeComposioRequest(
|
|
94
|
+
args: ComposioRequestArgs,
|
|
95
|
+
deps: ComposioRequestDeps = {},
|
|
96
|
+
): Promise<ComposioExecutionResult> {
|
|
97
|
+
const env = deps.env ?? process.env;
|
|
98
|
+
const fetchImpl = deps.fetch ?? fetch;
|
|
99
|
+
const apiKey = args.useOrgKey ? env.COMPOSIO_ORG_API_KEY : env.COMPOSIO_API_KEY;
|
|
100
|
+
const keyName = args.useOrgKey ? "COMPOSIO_ORG_API_KEY" : "COMPOSIO_API_KEY";
|
|
101
|
+
|
|
102
|
+
if (!apiKey) {
|
|
103
|
+
return {
|
|
104
|
+
body: null,
|
|
105
|
+
contentType: null,
|
|
106
|
+
error: `${keyName} is required. Bun auto-loads .env when running the CLI.`,
|
|
107
|
+
formattedBody: "",
|
|
108
|
+
method: args.method,
|
|
109
|
+
ok: false,
|
|
110
|
+
status: 0,
|
|
111
|
+
statusText: "Missing API key",
|
|
112
|
+
text: "",
|
|
113
|
+
url: buildComposioUrl(args.baseUrl, args.endpoint, args.query),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
registerVolatileSecret(apiKey, keyName);
|
|
118
|
+
|
|
119
|
+
const url = buildComposioUrl(args.baseUrl, args.endpoint, args.query);
|
|
120
|
+
const headers: Record<string, string> = {
|
|
121
|
+
...args.headers,
|
|
122
|
+
[args.useOrgKey ? "x-org-api-key" : "x-api-key"]: apiKey,
|
|
123
|
+
};
|
|
124
|
+
if (args.body !== undefined && !headers["Content-Type"] && !headers["content-type"]) {
|
|
125
|
+
headers["Content-Type"] = "application/json";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let response: Response;
|
|
129
|
+
try {
|
|
130
|
+
response = await fetchImpl(url, {
|
|
131
|
+
body: args.body === undefined ? undefined : JSON.stringify(args.body),
|
|
132
|
+
headers,
|
|
133
|
+
method: args.method,
|
|
134
|
+
});
|
|
135
|
+
} catch (err) {
|
|
136
|
+
return {
|
|
137
|
+
body: null,
|
|
138
|
+
contentType: null,
|
|
139
|
+
error: `request failed: ${scrubSecrets(errorMessage(err))}`,
|
|
140
|
+
formattedBody: "",
|
|
141
|
+
method: args.method,
|
|
142
|
+
ok: false,
|
|
143
|
+
status: 0,
|
|
144
|
+
statusText: "Request failed",
|
|
145
|
+
text: "",
|
|
146
|
+
url,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const text = await response.text();
|
|
151
|
+
const contentType = response.headers.get("Content-Type");
|
|
152
|
+
const body = parseResponseBody(text, contentType);
|
|
153
|
+
const formattedBody = formatResponseBody(text, contentType, args.raw);
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
body: scrubObject(body),
|
|
157
|
+
contentType,
|
|
158
|
+
formattedBody,
|
|
159
|
+
method: args.method,
|
|
160
|
+
ok: response.ok,
|
|
161
|
+
status: response.status,
|
|
162
|
+
statusText: response.statusText,
|
|
163
|
+
text: scrubSecrets(text),
|
|
164
|
+
url,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function composioArgsFromParts(input: {
|
|
169
|
+
baseUrl?: string;
|
|
170
|
+
body?: unknown;
|
|
171
|
+
endpoint: string;
|
|
172
|
+
headers?: Record<string, string>;
|
|
173
|
+
method: ComposioHttpMethod | string;
|
|
174
|
+
query?: Array<[string, string]> | Record<string, string | number | boolean | null | undefined>;
|
|
175
|
+
raw?: boolean;
|
|
176
|
+
useOrgKey?: boolean;
|
|
177
|
+
}): ComposioRequestArgs {
|
|
178
|
+
const method = input.method.toUpperCase();
|
|
179
|
+
if (!HTTP_METHODS.has(method)) {
|
|
180
|
+
throw new Error(`unsupported HTTP method: ${input.method}`);
|
|
181
|
+
}
|
|
182
|
+
if ((method === "GET" || method === "HEAD") && input.body !== undefined) {
|
|
183
|
+
throw new Error(`${method} requests cannot include a body`);
|
|
184
|
+
}
|
|
185
|
+
assertRelativeComposioPath(input.endpoint);
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
baseUrl: input.baseUrl || process.env.COMPOSIO_BASE_URL || DEFAULT_COMPOSIO_BASE_URL,
|
|
189
|
+
body: input.body,
|
|
190
|
+
endpoint: input.endpoint,
|
|
191
|
+
headers: input.headers ?? {},
|
|
192
|
+
method,
|
|
193
|
+
query: normalizeQuery(input.query),
|
|
194
|
+
raw: input.raw ?? false,
|
|
195
|
+
useOrgKey: input.useOrgKey ?? false,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function formatComposioResultForCli(result: ComposioExecutionResult): string {
|
|
200
|
+
if (result.formattedBody) return result.formattedBody;
|
|
201
|
+
return `HTTP ${result.status} ${result.statusText}`.trim();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function normalizeQuery(
|
|
205
|
+
query?: Array<[string, string]> | Record<string, string | number | boolean | null | undefined>,
|
|
206
|
+
): Array<[string, string]> {
|
|
207
|
+
if (!query) return [];
|
|
208
|
+
if (Array.isArray(query)) return query;
|
|
209
|
+
const pairs: Array<[string, string]> = [];
|
|
210
|
+
for (const [key, value] of Object.entries(query)) {
|
|
211
|
+
if (value === undefined || value === null) continue;
|
|
212
|
+
pairs.push([key, String(value)]);
|
|
213
|
+
}
|
|
214
|
+
return pairs;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function assertRelativeComposioPath(endpoint: string): void {
|
|
218
|
+
if (/^https?:\/\//i.test(endpoint)) {
|
|
219
|
+
throw new Error("endpoint must be a Composio API path, not an absolute URL");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function buildComposioUrl(
|
|
224
|
+
baseUrl: string,
|
|
225
|
+
endpoint: string,
|
|
226
|
+
query: Array<[string, string]>,
|
|
227
|
+
): string {
|
|
228
|
+
const normalizedBase = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
229
|
+
const cleanEndpoint = endpoint.replace(/^\/+/, "");
|
|
230
|
+
const url = new URL(cleanEndpoint, normalizedBase);
|
|
231
|
+
for (const [key, value] of query) {
|
|
232
|
+
url.searchParams.append(key, value);
|
|
233
|
+
}
|
|
234
|
+
return url.toString();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function formatResponseBody(text: string, contentType: string | null, raw: boolean): string {
|
|
238
|
+
if (raw) return scrubSecrets(text);
|
|
239
|
+
const body = parseResponseBody(text, contentType);
|
|
240
|
+
if (body === null || body === "") return "";
|
|
241
|
+
if (body !== text) return scrubSecrets(JSON.stringify(body, null, 2));
|
|
242
|
+
return scrubSecrets(text);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function parseResponseBody(text: string, contentType: string | null): unknown {
|
|
246
|
+
const trimmed = text.trim();
|
|
247
|
+
if (!trimmed) return "";
|
|
248
|
+
if (contentType?.includes("json") || trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
249
|
+
try {
|
|
250
|
+
return JSON.parse(trimmed);
|
|
251
|
+
} catch {
|
|
252
|
+
return text;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return text;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function parseJsonArg(value: string, flag: string): unknown {
|
|
259
|
+
try {
|
|
260
|
+
return JSON.parse(value);
|
|
261
|
+
} catch (err) {
|
|
262
|
+
throw new Error(`${flag} must be valid JSON: ${errorMessage(err)}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function parsePair(raw: string, flag: string): [string, string] {
|
|
267
|
+
const index = raw.indexOf("=");
|
|
268
|
+
if (index <= 0) {
|
|
269
|
+
throw new Error(`${flag} must use key=value`);
|
|
270
|
+
}
|
|
271
|
+
return [raw.slice(0, index), raw.slice(index + 1)];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function parseHeader(raw: string): [string, string] {
|
|
275
|
+
const equalsIndex = raw.indexOf("=");
|
|
276
|
+
const colonIndex = raw.indexOf(":");
|
|
277
|
+
const index =
|
|
278
|
+
colonIndex > 0 && (equalsIndex === -1 || colonIndex < equalsIndex) ? colonIndex : equalsIndex;
|
|
279
|
+
if (index <= 0) {
|
|
280
|
+
throw new Error("--header must use Name=value or Name: value");
|
|
281
|
+
}
|
|
282
|
+
return [raw.slice(0, index).trim(), raw.slice(index + 1).trim()];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function requiredValue(argv: string[], index: number, flag: string): string {
|
|
286
|
+
const value = argv[index];
|
|
287
|
+
if (!value || value.startsWith("--")) {
|
|
288
|
+
throw new Error(`${flag} requires a value`);
|
|
289
|
+
}
|
|
290
|
+
return value;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function errorMessage(err: unknown): string {
|
|
294
|
+
return err instanceof Error ? err.message : String(err);
|
|
295
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: attio-interaction
|
|
3
|
+
description: "How to read and write your Attio CRM via the REST API v2: query/filter records, upsert companies/people/deals with matching_attribute, write notes and tasks, manage list entries, and handle webhooks. Auth via ATTIO_API_KEY (Bearer token). Use this skill whenever you need to interact with Attio CRM data from the swarm."
|
|
4
|
+
user-invocable: false
|
|
5
|
+
agentAutoTrigger: When asked to read from, write to, search, query, or update Attio CRM records, deals, companies, people, notes, tasks, or lists. Also when a task references Attio data such as pipeline, ICP scoring, lead enrichment, stale deals, CRM hygiene, deal handoffs, or logging an interaction to Attio.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Attio Interaction (Read + Write)
|
|
9
|
+
|
|
10
|
+
Use this skill to read and write your Attio CRM through the REST API v2. Every read or write is a direct API call; agent-swarm does not maintain a separate Attio sync.
|
|
11
|
+
|
|
12
|
+
## TL;DR
|
|
13
|
+
|
|
14
|
+
1. Resolve `ATTIO_API_KEY` from swarm config before making calls.
|
|
15
|
+
2. Base URL: `https://api.attio.com/v2/`
|
|
16
|
+
3. Use `Authorization: Bearer $ATTIO_API_KEY`, `Content-Type: application/json`, and `Accept: application/json`.
|
|
17
|
+
4. Prefer upsert over create for People, Companies, and Deals: `PUT /v2/objects/{slug}/records` with `matching_attribute`.
|
|
18
|
+
5. Rate limits: 100 reads/sec, 25 writes/sec. Pace write bursts to roughly 15-20/sec.
|
|
19
|
+
6. Attribute values are arrays, even for scalar values: `[{ "value": 42 }]`, never `42`.
|
|
20
|
+
|
|
21
|
+
## Authentication
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
ATTIO_API_KEY=$(get-config key="ATTIO_API_KEY" includeSecrets=true)
|
|
25
|
+
|
|
26
|
+
curl -sS "https://api.attio.com/v2/objects" \
|
|
27
|
+
-H "Authorization: Bearer $ATTIO_API_KEY" \
|
|
28
|
+
-H "Accept: application/json"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
If calls return `401`, re-fetch the key from config. If it still fails, notify the Lead; the key may need rotation. Do not retry silently.
|
|
32
|
+
|
|
33
|
+
## Core object slugs
|
|
34
|
+
|
|
35
|
+
| Object | API slug | Primary matching attribute |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| Companies | `companies` | `domains` |
|
|
38
|
+
| People | `people` | `email_addresses` |
|
|
39
|
+
| Deals | `deals` | Usually no global dedupe key; link to company/person records |
|
|
40
|
+
|
|
41
|
+
Custom objects use their configured API slug. Discover them with `GET /v2/objects`.
|
|
42
|
+
|
|
43
|
+
## Common operations
|
|
44
|
+
|
|
45
|
+
### 1. Discover objects and slugs
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
curl -sS "https://api.attio.com/v2/objects" \
|
|
49
|
+
-H "Authorization: Bearer $ATTIO_API_KEY" \
|
|
50
|
+
-H "Accept: application/json" \
|
|
51
|
+
| jq '.data[] | {slug: .api_slug, name: .title}'
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 2. Query records with filters and pagination
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
curl -sS -X POST "https://api.attio.com/v2/objects/companies/records/query" \
|
|
58
|
+
-H "Authorization: Bearer $ATTIO_API_KEY" \
|
|
59
|
+
-H "Content-Type: application/json" \
|
|
60
|
+
-d '{
|
|
61
|
+
"filter": {
|
|
62
|
+
"stage": { "$not_equal": "Won" }
|
|
63
|
+
},
|
|
64
|
+
"limit": 100,
|
|
65
|
+
"offset": 0
|
|
66
|
+
}' | jq '.data[] | {record_id: .id.record_id, name: .values.name[0].value}'
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Paginate by increasing `offset` until `data` is empty. For no filter, omit the `filter` key.
|
|
70
|
+
|
|
71
|
+
### 3. Get a single record
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
curl -sS "https://api.attio.com/v2/objects/companies/records/{RECORD_ID}" \
|
|
75
|
+
-H "Authorization: Bearer $ATTIO_API_KEY" \
|
|
76
|
+
-H "Accept: application/json" \
|
|
77
|
+
| jq '.data.values'
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 4. Upsert a company by domain
|
|
81
|
+
|
|
82
|
+
Use `PUT` with `matching_attribute`. It creates if not found and updates if found, so it is safe to call repeatedly.
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
curl -sS -X PUT "https://api.attio.com/v2/objects/companies/records" \
|
|
86
|
+
-H "Authorization: Bearer $ATTIO_API_KEY" \
|
|
87
|
+
-H "Content-Type: application/json" \
|
|
88
|
+
-d '{
|
|
89
|
+
"data": {
|
|
90
|
+
"values": {
|
|
91
|
+
"name": [{ "value": "Acme Corp" }],
|
|
92
|
+
"domains": [{ "domain": "acme.com" }],
|
|
93
|
+
"employee_count": [{ "value": 150 }]
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
"matching_attribute": "domains"
|
|
97
|
+
}' | jq '{record_id: .data.id.record_id}'
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 5. Upsert a person by email
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
curl -sS -X PUT "https://api.attio.com/v2/objects/people/records" \
|
|
104
|
+
-H "Authorization: Bearer $ATTIO_API_KEY" \
|
|
105
|
+
-H "Content-Type: application/json" \
|
|
106
|
+
-d '{
|
|
107
|
+
"data": {
|
|
108
|
+
"values": {
|
|
109
|
+
"name": [{ "first_name": "Jane", "last_name": "Doe" }],
|
|
110
|
+
"email_addresses": [{ "email_address": "jane@acme.com" }],
|
|
111
|
+
"job_title": [{ "value": "CTO" }]
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
"matching_attribute": "email_addresses"
|
|
115
|
+
}' | jq '{record_id: .data.id.record_id}'
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 6. Update specific attributes on an existing record
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
curl -sS -X PATCH "https://api.attio.com/v2/objects/companies/records/{RECORD_ID}" \
|
|
122
|
+
-H "Authorization: Bearer $ATTIO_API_KEY" \
|
|
123
|
+
-H "Content-Type: application/json" \
|
|
124
|
+
-d '{
|
|
125
|
+
"data": {
|
|
126
|
+
"values": {
|
|
127
|
+
"icp_score": [{ "value": 85 }],
|
|
128
|
+
"icp_tier": [{ "value": "Tier 1" }]
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}'
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### 7. Write a note to a record
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
curl -sS -X POST "https://api.attio.com/v2/notes" \
|
|
138
|
+
-H "Authorization: Bearer $ATTIO_API_KEY" \
|
|
139
|
+
-H "Content-Type: application/json" \
|
|
140
|
+
-d '{
|
|
141
|
+
"data": {
|
|
142
|
+
"parent_object": "companies",
|
|
143
|
+
"parent_record_id": "{RECORD_ID}",
|
|
144
|
+
"title": "Enrichment - 2026-06-02",
|
|
145
|
+
"content": "Employee count: 150. Funding stage: Series A. Tech stack: Node.js, React."
|
|
146
|
+
}
|
|
147
|
+
}' | jq '{note_id: .data.id.note_id}'
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### 8. Create a task linked to a record
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
curl -sS "https://api.attio.com/v2/workspace_members" \
|
|
154
|
+
-H "Authorization: Bearer $ATTIO_API_KEY" \
|
|
155
|
+
-H "Accept: application/json" \
|
|
156
|
+
| jq '.data[] | {member_id: .id.workspace_member_id, name: .name, email: .email_address}'
|
|
157
|
+
|
|
158
|
+
curl -sS -X POST "https://api.attio.com/v2/tasks" \
|
|
159
|
+
-H "Authorization: Bearer $ATTIO_API_KEY" \
|
|
160
|
+
-H "Content-Type: application/json" \
|
|
161
|
+
-d '{
|
|
162
|
+
"data": {
|
|
163
|
+
"content": "Follow up - no contact in 21 days",
|
|
164
|
+
"is_completed": false,
|
|
165
|
+
"assignees": [
|
|
166
|
+
{ "referenced_actor_type": "workspace-member", "referenced_actor_id": "{MEMBER_ID}" }
|
|
167
|
+
],
|
|
168
|
+
"linked_records": [
|
|
169
|
+
{ "target_object": "deals", "target_record_id": "{DEAL_RECORD_ID}" }
|
|
170
|
+
]
|
|
171
|
+
}
|
|
172
|
+
}' | jq '{task_id: .data.id.task_id}'
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### 9. Post a comment on a record
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
curl -sS -X POST "https://api.attio.com/v2/comments" \
|
|
179
|
+
-H "Authorization: Bearer $ATTIO_API_KEY" \
|
|
180
|
+
-H "Content-Type: application/json" \
|
|
181
|
+
-d '{
|
|
182
|
+
"data": {
|
|
183
|
+
"record": { "target_object": "companies", "target_record_id": "{RECORD_ID}" },
|
|
184
|
+
"content": [
|
|
185
|
+
{ "type": "text", "text": "Possible duplicate of acme-corp-old - please review and merge." }
|
|
186
|
+
]
|
|
187
|
+
}
|
|
188
|
+
}' | jq '{comment_id: .data.id.comment_id}'
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### 10. Query a list or pipeline view
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
curl -sS "https://api.attio.com/v2/lists" \
|
|
195
|
+
-H "Authorization: Bearer $ATTIO_API_KEY" \
|
|
196
|
+
-H "Accept: application/json" \
|
|
197
|
+
| jq '.data[] | {list_id: .id.list_id, name: .title}'
|
|
198
|
+
|
|
199
|
+
curl -sS -X POST "https://api.attio.com/v2/lists/{LIST_ID}/entries/query" \
|
|
200
|
+
-H "Authorization: Bearer $ATTIO_API_KEY" \
|
|
201
|
+
-H "Content-Type: application/json" \
|
|
202
|
+
-d '{ "limit": 100, "offset": 0 }' \
|
|
203
|
+
| jq '.data[] | {entry_id: .id.entry_id, record_id: .record_id}'
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### 11. Global search across objects
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
curl -sS -X POST "https://api.attio.com/v2/records/search" \
|
|
210
|
+
-H "Authorization: Bearer $ATTIO_API_KEY" \
|
|
211
|
+
-H "Content-Type: application/json" \
|
|
212
|
+
-d '{ "query": "acme", "limit": 10 }' \
|
|
213
|
+
| jq '.data[] | {object: .object_type, record_id: .id.record_id}'
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Webhooks
|
|
217
|
+
|
|
218
|
+
Attio delivers webhooks at least once. Payloads contain IDs only, so always re-fetch the full record via `GET` before acting.
|
|
219
|
+
|
|
220
|
+
Key event types:
|
|
221
|
+
|
|
222
|
+
- `record.created`, `record.updated`, `record.deleted`
|
|
223
|
+
- `list-entry.created`, `list-entry.updated`, `list-entry.deleted`
|
|
224
|
+
- `note.created`
|
|
225
|
+
- `task.created`, `task.completed`
|
|
226
|
+
|
|
227
|
+
Webhook timeout is 5 seconds. Respond `200` immediately and do async processing in a follow-up swarm task.
|
|
228
|
+
|
|
229
|
+
## Rate limits
|
|
230
|
+
|
|
231
|
+
| Operation | Hard limit | Safe working rate |
|
|
232
|
+
|---|---|---|
|
|
233
|
+
| Reads | 100 req/sec | ~80 req/sec |
|
|
234
|
+
| Writes | 25 req/sec | ~15-20 req/sec |
|
|
235
|
+
|
|
236
|
+
Add `sleep 0.05` between write calls in loops. Attio does not provide a native batch endpoint for these operations.
|
|
237
|
+
|
|
238
|
+
## Operational rules
|
|
239
|
+
|
|
240
|
+
- Upsert first. Use `PUT /records` with `matching_attribute` for create-or-update. `POST /records` can create duplicates.
|
|
241
|
+
- Re-fetch webhook records. Webhook payloads are event hints, not full source-of-truth records.
|
|
242
|
+
- Values are arrays. Every attribute value must be wrapped in an array.
|
|
243
|
+
- No merge endpoint. Attio has no API-level record merge; dedupe agents should flag duplicates as comments or tasks for human review.
|
|
244
|
+
- Check config first. Fetch `ATTIO_API_KEY` via `get-config includeSecrets=true`; never hardcode it.
|
|
245
|
+
|
|
246
|
+
## Error handling
|
|
247
|
+
|
|
248
|
+
| Status | Likely cause | Action |
|
|
249
|
+
|---|---|---|
|
|
250
|
+
| 401 | API key invalid or expired | Re-fetch from config. If still failing, notify Lead for rotation. |
|
|
251
|
+
| 403 | Key lacks permission | Check the Attio API key's workspace permissions. |
|
|
252
|
+
| 404 | Wrong object slug or record ID | Re-discover slugs with `GET /v2/objects`. |
|
|
253
|
+
| 400 | Malformed body | Ensure attribute values are wrapped in arrays. |
|
|
254
|
+
| 422 | Validation or conflict error | Read the `errors` array for field-level details. |
|
|
255
|
+
| 429 | Rate-limited | Back off and retry after `Retry-After` if provided. |
|
|
256
|
+
|
|
257
|
+
## Worked example: stale deal reactivation
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
ATTIO_API_KEY=$(get-config key="ATTIO_API_KEY" includeSecrets=true)
|
|
261
|
+
|
|
262
|
+
DEALS=$(curl -sS -X POST "https://api.attio.com/v2/objects/deals/records/query" \
|
|
263
|
+
-H "Authorization: Bearer $ATTIO_API_KEY" \
|
|
264
|
+
-H "Content-Type: application/json" \
|
|
265
|
+
-d '{"filter": {"stage": {"$not_equal": "Won"}}, "limit": 200}')
|
|
266
|
+
|
|
267
|
+
echo "$DEALS" | jq -r '.data[] | .id.record_id' | while read -r RECORD_ID; do
|
|
268
|
+
RECORD=$(curl -sS "https://api.attio.com/v2/objects/deals/records/$RECORD_ID" \
|
|
269
|
+
-H "Authorization: Bearer $ATTIO_API_KEY" \
|
|
270
|
+
-H "Accept: application/json")
|
|
271
|
+
# Compute staleness from the relevant date attribute. If stale, create a task.
|
|
272
|
+
sleep 0.05
|
|
273
|
+
done
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Related references
|
|
277
|
+
|
|
278
|
+
- Official Attio REST API docs: https://developers.attio.com/reference
|
|
279
|
+
- Official Attio MCP overview: https://docs.attio.com/mcp/overview
|