Skip to content

Commit ea0e712

Browse files
MLuc24michelle0927claude
authored
fix(attio): give Create Task and Create or Update Record a static prop set (#21768)
* fix(attio): give Create Task and Create or Update Record a static prop set Both actions built their inputs in additionalProps(), driven by reloadProps, and both fetched remote data to do it. An MCP tool is registered with a single static JSON Schema, so props that only exist after a reload never reach an agent. Create Task generated a targetObject/targetRecordId pair per linked record, counted by numberOfLinkedRecords. It now takes linkedRecords, a list of JSON objects, and reports a malformed or incomplete entry instead of sending a half-built link. Create or Update Record fetched the object's writable attributes and emitted a prop per attribute. It now takes values, one object of attribute slug to value, the shape the Salesforce and HubSpot conversions settled on. objectId and matchingAttribute keep their remote options and their dependency on each other; only the reload goes. That also fixes what run() sent: it collected everything on his that was not a known prop, so the action's own methods travelled to Attio as attribute values. values is an explicit prop now, so only values are sent. * fix(attio): name the input when Values will not parse A malformed JSON string reached the caller as a raw SyntaxError, while the sibling create-task action says which linked record is wrong. Values is parsed once, before the request is built, and a parse failure now says so. * fix(attio): validate Values object + reconcile create-update-record description - create-update-record: reject non-object Values (null, arrays, scalars) with a clear ConfigurationError before the upsert, instead of passing them to the API. - Description: swap the misleading "a deal" example (deals are filtered out of the object options because upsert needs a unique matching attribute they lack) for "a company", and note that objects without a unique attribute aren't available. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(attio): AI-optimize action descriptions + mark components optimized agent-audit raised attio from ~52 to 100/100. All 8 action descriptions now carry behavioral guidance ("Use when…"/"Returns…"), a concrete worked example, and cross-references to lookup actions (e.g. delete-list-entry → List List ID Options; update-person → Get Record). Marked every action file and the app file with `// x-pd-ai: optimized`. No behavior change — descriptions/metadata only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(attio): add List Records action Adds an AI-optimized list/search action for records of any object (people, companies, deals, custom) — wraps the existing records query endpoint. Fills the gap where the toolset could create/update/get records but not enumerate them, which forced agents to (mis)use the record-id option loader as a backdoor. Discoverable Object prop, bounded Limit (1–500), Offset paging with follow-up guidance, and an additive Fields projection to trim payload. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(attio): reference List Records in record-id prop descriptions + version bumps - All record-id-based prop descriptions (app-level recordId, get-record, update-person Person ID/Company ID, create-person Company ID, create-note Parent Record ID) now point the agent to the **List Records** action to look up record IDs. - Version bumps required by the shared app-file change + new List Records action: 6 actions and all 10 sources patched; package.json 0.6.0 -> 0.7.0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(attio): keep package.json at 0.6.0 The 0.6.0 bump already on this PR covers the component changes; revert the extra 0.7.0 bump. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Michelle Bergeron <michelle.bergeron@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: michelle0927 <michelle0927@users.noreply.github.com>
1 parent 68fb36a commit ea0e712

21 files changed

Lines changed: 185 additions & 163 deletions

File tree

components/attio/actions/create-note/create-note.mjs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
// x-pd-ai: optimized
12
import attio from "../../attio.app.mjs";
23

34
export default {
45
key: "attio-create-note",
56
name: "Create Note",
6-
description: "Creates a new note for a given record. The note will be linked to the specified record. [See the documentation](https://developers.attio.com/reference/post_v2-notes)",
7-
version: "0.0.6",
7+
description: "Create a plaintext note attached to a record (person, company, deal, or any object). Use when you want to log context on a record. Set **Parent Object**, **Parent Record ID**, **Title**, and **Content**. Example: Parent Object `people`, Parent Record ID `891dcbfc-9141-415d-9b2a-2238a6cc012d`, Title `Intro call`, Content `Discussed the Q3 rollout timeline`. Returns the created note with its id. [See the documentation](https://developers.attio.com/reference/post_v2-notes)",
8+
version: "0.0.7",
89
annotations: {
910
destructiveHint: false,
1011
openWorldHint: true,
@@ -39,7 +40,7 @@ export default {
3940
}),
4041
],
4142
label: "Parent Record ID",
42-
description: "The ID of the parent record the note belongs to",
43+
description: "The ID of the parent record the note belongs to. Use the **List Records** action to look up record IDs.",
4344
},
4445
title: {
4546
type: "string",

components/attio/actions/create-person/create-person.mjs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
// x-pd-ai: optimized
12
import attio from "../../attio.app.mjs";
23
import constants from "../../common/constants.mjs";
34

45
export default {
56
key: "attio-create-person",
67
name: "Create Person",
7-
description: "Creates a new person. [See the documentation](https://developers.attio.com/reference/post_v2-objects-people-records).",
8-
version: "0.0.4",
8+
description: "Create a new person record in Attio. Use when adding a contact. Set any of the person fields (name, email, job title, phone, socials) and optionally link a company by its record id. Example: First Name `Ada`, Last Name `Lovelace`, Email `ada@example.com`, Job Title `Analyst`. Returns the created person record with its id. [See the documentation](https://developers.attio.com/reference/post_v2-objects-people-records).",
9+
version: "0.0.5",
910
annotations: {
1011
destructiveHint: false,
1112
openWorldHint: true,
@@ -82,7 +83,7 @@ export default {
8283
},
8384
companyId: {
8485
label: "Company ID",
85-
description: "The ID of the company to associate with the person.",
86+
description: "The ID of the company to associate with the person. Use the **List Records** action (object `companies`) to look up company record IDs.",
8687
optional: true,
8788
propDefinition: [
8889
attio,

components/attio/actions/create-task/create-task.mjs

Lines changed: 31 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1+
// x-pd-ai: optimized
2+
import { ConfigurationError } from "@pipedream/platform";
13
import attio from "../../attio.app.mjs";
2-
import constants from "../../common/constants.mjs";
3-
import utils from "../../common/utils.mjs";
44

55
export default {
66
key: "attio-create-task",
77
name: "Create Task",
8-
description: "Creates a new task. [See the documentation](https://docs.attio.com/rest-api/endpoint-reference/tasks/create-a-task)",
9-
version: "0.0.4",
8+
description: "Create a task in Attio with content, a deadline, completion state, optional assignees, and optional linked records. Use when you need a follow-up or to-do. Example: Content `Email the signed contract to Ada`, Deadline `2026-09-01T17:00:00Z`, Is Completed `false`. Returns the created task with its id. [See the documentation](https://docs.attio.com/rest-api/endpoint-reference/tasks/create-a-task)",
9+
version: "0.1.0",
1010
annotations: {
1111
destructiveHint: false,
1212
openWorldHint: true,
@@ -39,102 +39,50 @@ export default {
3939
"workspaceMemberId",
4040
],
4141
},
42-
numberOfLinkedRecords: {
43-
type: "integer",
44-
label: "Number Of Linked Records",
45-
description: "The number of linked records to generate. Defaults to 1.",
46-
default: 1,
47-
reloadProps: true,
42+
linkedRecords: {
43+
type: "string[]",
44+
label: "Linked Records",
45+
description: "Records to link to the task. Each entry is a JSON object naming the object type and the record, e.g. `{ \"target_object\": \"companies\", \"target_record_id\": \"891dcbfc-9141-415d-9b2a-2238a6cc012d\" }`. `target_object` accepts `companies`, `people`, `users`, `workspaces` or `deals`.",
46+
optional: true,
4847
},
4948
},
5049
methods: {
51-
linkedRecordPropsMapper(prefix) {
52-
const {
53-
[`${prefix}targetObject`]: targetObject,
54-
[`${prefix}targetRecordId`]: targetRecordId,
55-
} = this;
56-
57-
return {
58-
target_object: targetObject,
59-
target_record_id: targetRecordId,
60-
};
61-
},
62-
async getLinkedRecordsPropDefinitions({
63-
prefix, label,
64-
} = {}) {
65-
const targetObjectPropName = `${prefix}targetObject`;
66-
const targetObject = this[targetObjectPropName];
67-
let targetRecordOptions = [];
68-
69-
if (targetObject) {
70-
const { data } = await this.attio.listRecords({
71-
targetObject,
72-
data: {
73-
limit: 100,
74-
sorts: [
75-
{
76-
direction: "desc",
77-
attribute: "created_at",
78-
field: "value",
79-
},
80-
],
81-
},
82-
});
83-
84-
targetRecordOptions = data.map(({
85-
id: { record_id: value },
86-
values: { name },
87-
}) => ({
88-
value,
89-
label: name[0]?.value || name[0]?.full_name || "unknown",
90-
}));
91-
}
92-
93-
return {
94-
[targetObjectPropName]: {
95-
type: "string",
96-
label: `${label} - Target Object`,
97-
description: "The type of the record to link to. Eg. `companies`, `contacts`, `deals`",
98-
options: Object.values(constants.TARGET_OBJECT),
99-
reloadProps: true,
100-
},
101-
[`${prefix}targetRecordId`]: {
102-
type: "string",
103-
label: `${label} - Target Record ID`,
104-
description: "The ID of the record to link to",
105-
options: targetRecordOptions,
106-
},
107-
};
108-
},
10950
createTask(args = {}) {
11051
return this.attio.post({
11152
path: "/tasks",
11253
...args,
11354
});
11455
},
11556
},
116-
async additionalProps() {
117-
const {
118-
numberOfLinkedRecords,
119-
getLinkedRecordsPropDefinitions,
120-
} = this;
121-
122-
return utils.getAdditionalProps({
123-
numberOfFields: numberOfLinkedRecords,
124-
fieldName: "linked record",
125-
getPropDefinitions: getLinkedRecordsPropDefinitions,
126-
});
127-
},
12857
async run({ $ }) {
12958
const {
13059
content,
13160
deadlineAt,
13261
isCompleted,
13362
assigneeIds,
134-
numberOfLinkedRecords,
135-
linkedRecordPropsMapper,
63+
linkedRecords,
13664
} = this;
13765

66+
// Each entry arrives as JSON, so a malformed one should say so rather
67+
// than reach Attio as an incomplete link.
68+
const parsedLinkedRecords = (linkedRecords ?? []).map((record, index) => {
69+
let parsed;
70+
try {
71+
parsed = typeof record === "string"
72+
? JSON.parse(record)
73+
: record;
74+
} catch (error) {
75+
throw new ConfigurationError(`Linked record ${index + 1} is not valid JSON: ${error.message}`);
76+
}
77+
if (!parsed?.target_object || !parsed?.target_record_id) {
78+
throw new ConfigurationError(`Linked record ${index + 1} needs both \`target_object\` and \`target_record_id\`.`);
79+
}
80+
return {
81+
target_object: parsed.target_object,
82+
target_record_id: parsed.target_record_id,
83+
};
84+
});
85+
13886
const response = await this.createTask({
13987
$,
14088
data: {
@@ -147,11 +95,7 @@ export default {
14795
referenced_actor_type: "workspace-member",
14896
referenced_actor_id: id,
14997
})) || [],
150-
linked_records: utils.getFieldsProps({
151-
numberOfFields: numberOfLinkedRecords,
152-
fieldName: "linked record",
153-
propsMapper: linkedRecordPropsMapper,
154-
}),
98+
linked_records: parsedLinkedRecords,
15599
},
156100
},
157101
});

components/attio/actions/create-update-record/create-update-record.mjs

Lines changed: 29 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1+
// x-pd-ai: optimized
2+
import { ConfigurationError } from "@pipedream/platform";
13
import attio from "../../attio.app.mjs";
24
import constants from "../../common/constants.mjs";
3-
import utils from "../../common/utils.mjs";
45

56
export default {
67
key: "attio-create-update-record",
78
name: "Create or Update Record",
8-
description: "Creates or updates a specific record such as a person or a deal. If the record already exists, it's updated. Otherwise, a new record is created. [See the documentation](https://developers.attio.com/reference/put_v2-objects-object-records)",
9-
version: "0.0.6",
9+
description: "Creates or updates a specific record such as a person or a company. If a record with the same matching attribute already exists, it's updated; otherwise a new record is created. Objects without a unique attribute to match on (e.g. deals) are not available here. Example: Object `companies`, Matching Attribute `domains`, Values `{ \"domains\": [\"acme.com\"], \"name\": \"Acme\" }`. Returns the created or updated record with its id. [See the documentation](https://developers.attio.com/reference/put_v2-objects-object-records)",
10+
version: "0.1.0",
1011
annotations: {
1112
destructiveHint: true,
1213
openWorldHint: true,
@@ -16,7 +17,6 @@ export default {
1617
props: {
1718
attio,
1819
objectId: {
19-
reloadProps: true,
2020
propDefinition: [
2121
attio,
2222
"objectId",
@@ -26,7 +26,6 @@ export default {
2626
],
2727
},
2828
matchingAttribute: {
29-
reloadProps: true,
3029
propDefinition: [
3130
attio,
3231
"matchingAttribute",
@@ -35,56 +34,39 @@ export default {
3534
}),
3635
],
3736
},
38-
},
39-
async additionalProps() {
40-
const props = {};
41-
if (!this.matchingAttribute) {
42-
return props;
43-
}
44-
const attributes = await this.getRelevantAttributes();
45-
46-
const matchingAttribute = attributes.find(
47-
(a) => a.api_slug === this.matchingAttribute,
48-
);
49-
if (matchingAttribute) {
50-
attributes.splice(attributes.indexOf(matchingAttribute), 1);
51-
attributes.unshift(matchingAttribute);
52-
}
53-
54-
for (const attribute of attributes) {
55-
props[attribute.api_slug] = {
56-
type: attribute.is_multiselect
57-
? "string[]"
58-
: "string",
59-
label: attribute.title,
60-
optional: !attribute.is_required,
61-
};
62-
}
63-
return props;
64-
},
65-
methods: {
66-
async getRelevantAttributes() {
67-
const stream = utils.paginate({
68-
fn: this.attio.listAttributes,
69-
args: {
70-
objectId: this.objectId,
71-
},
72-
});
73-
const attributes = await utils.streamIterator(stream);
74-
75-
return attributes
76-
.filter((a) => a.is_writable)
77-
.sort((a, b) => b.is_required - a.is_required);
37+
values: {
38+
type: "object",
39+
label: "Values",
40+
description: "Attribute slug to value pairs for the record, e.g. `{ \"name\": \"Ada Lovelace\", \"email_addresses\": [\"ada@example.test\"] }`. Include the matching attribute among them. [See the attributes endpoint](https://developers.attio.com/reference/get_v2-target-identifier-attributes) for what a given object accepts.",
7841
},
7942
},
8043
async run({ $ }) {
8144
const {
8245
attio,
8346
objectId,
8447
matchingAttribute,
85-
...values
48+
values,
8649
} = this;
8750

51+
let parsedValues;
52+
try {
53+
parsedValues = typeof values === "string"
54+
? JSON.parse(values)
55+
: values;
56+
} catch (error) {
57+
throw new ConfigurationError(`Values is not valid JSON: ${error.message}`);
58+
}
59+
60+
if (
61+
parsedValues === null
62+
|| typeof parsedValues !== "object"
63+
|| Array.isArray(parsedValues)
64+
) {
65+
throw new ConfigurationError(
66+
"Values must be a JSON object of attribute slug to value pairs (not a list, null, or a single value).",
67+
);
68+
}
69+
8870
const response = await attio.upsertRecord({
8971
$,
9072
objectId,
@@ -93,7 +75,7 @@ export default {
9375
},
9476
data: {
9577
data: {
96-
values,
78+
values: parsedValues,
9779
},
9880
},
9981
});

components/attio/actions/delete-list-entry/delete-list-entry.mjs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
// x-pd-ai: optimized
12
import attio from "../../attio.app.mjs";
23

34
export default {
45
key: "attio-delete-list-entry",
56
name: "Delete List Entry",
6-
description: "Deletes an existing entry from a specific list. [See the documentation](https://developers.attio.com/reference/delete_v2-lists-list-entries-entry-id)",
7-
version: "0.0.6",
7+
description: "Delete an entry from a list (this removes the record from the list, not the underlying record itself). Destructive — confirm the entry before deleting. Use **List List ID Options** to find a **List ID**, then choose the **Entry ID** to remove. Example: List ID `33ebdbe9-e529-47c9-b894-0ba25e9c15c0`, Entry ID `2e6e29ea-c4e9-4f4b-9b1e-7f2c1a0d9c11`. Returns the API confirmation. [See the documentation](https://developers.attio.com/reference/delete_v2-lists-list-entries-entry-id)",
8+
version: "0.0.7",
89
annotations: {
910
destructiveHint: true,
1011
openWorldHint: true,

components/attio/actions/get-record/get-record.mjs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
// x-pd-ai: optimized
12
import attio from "../../attio.app.mjs";
23

34
export default {
45
key: "attio-get-record",
56
name: "Get Record",
6-
description: "Retrieves the record with the specified ID. [See the documentation](https://docs.attio.com/rest-api/endpoint-reference/records/get-a-record)",
7-
version: "0.0.3",
7+
description: "Retrieve a single record by its ID from a given object (people, companies, deals, or a custom object). Use when you have a record's id and need its attribute values. Example: Object `people`, Record ID `891dcbfc-9141-415d-9b2a-2238a6cc012d`. Returns the record with its attribute values. [See the documentation](https://docs.attio.com/rest-api/endpoint-reference/records/get-a-record)",
8+
version: "0.0.4",
89
annotations: {
910
destructiveHint: false,
1011
openWorldHint: true,
@@ -20,7 +21,7 @@ export default {
2021
],
2122
},
2223
recordId: {
23-
description: "The identifier of the record to retrieve.",
24+
description: "The identifier of the record to retrieve. Use the **List Records** action to look up record IDs.",
2425
propDefinition: [
2526
attio,
2627
"recordId",

components/attio/actions/list-list-id-options/list-list-id-options.mjs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
// x-pd-ai: optimized
12
import attio from "../../attio.app.mjs";
23

34
export default {
45
key: "attio-list-list-id-options",
56
name: "List List ID Options",
6-
description: "Retrieves available options for the List ID field.",
7-
version: "0.0.1",
7+
description: "List the workspace's lists as {value, label} options, to discover a **List ID** to pass to other actions (e.g. Delete List Entry). Use when you need a list's id and don't have it. Takes no input. Example: call with no arguments; returns e.g. `[{ \"value\": \"33ebdbe9-e529-47c9-b894-0ba25e9c15c0\", \"label\": \"Hot Leads\" }]`. [See the documentation](https://developers.attio.com/reference/get_v2-lists)",
8+
version: "0.0.2",
89
type: "action",
910
annotations: {
1011
destructiveHint: false,

0 commit comments

Comments
 (0)