Skip to content

Commit 2d53a06

Browse files
authored
feat: Instant triggers via public webhook API + agent/project/task actions (PR #20)
TRIGGERS (public webhook API, live since v6.188): new_comment, task_assigned, new_project, project_assigned, project_joined — one factory over POST /api/v2/subscribeWebhook|unsubscribeWebhook with exact server packet shapes. Legacy task_due untouched. ACTIONS: AI-agent lifecycle (create/generate/update, add knowledge, publish), project ops (complete/restore, copy, share link), task extras (set date, assign, note, custom field), and Trigger Taskade Automation (POST to a flow's inbound webhook URL — Taskade's documented integration pattern). n8n: TaskadeTrigger node — the package's first trigger, 6 events via the public subscription lifecycle; robust delete() clears local hook state even when the server hook is already gone. Pre-PR review applied 8 fixes: implicit-now date guard, share-link viewUrl mapping, custom-field label path (data.displayName), trigger delete resilience, automation-webhook failure surfacing, Infinity coercion guard, assignee empty-string filter, conditional-field pre-flight errors.
1 parent 6858902 commit 2d53a06

21 files changed

Lines changed: 1196 additions & 3 deletions

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "taskade",
3-
"version": "1.1.0",
3+
"version": "1.2.0",
44
"description": "Taskade is a collaboration platform for remote teams to organize and manage projects.",
55
"main": "index.js",
66
"scripts": {
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import type {
2+
IDataObject,
3+
IHookFunctions,
4+
INodeType,
5+
INodeTypeDescription,
6+
IWebhookFunctions,
7+
IWebhookResponseData,
8+
} from 'n8n-workflow';
9+
import { NodeConnectionTypes } from 'n8n-workflow';
10+
11+
import { taskadeApiRequest } from './GenericFunctions';
12+
13+
export class TaskadeTrigger implements INodeType {
14+
description: INodeTypeDescription = {
15+
displayName: 'Taskade Trigger',
16+
name: 'taskadeTrigger',
17+
icon: 'file:taskade.svg',
18+
group: ['trigger'],
19+
version: 1,
20+
subtitle: '={{$parameter["event"]}}',
21+
description: 'Starts a workflow when something happens in Taskade',
22+
defaults: { name: 'Taskade Trigger' },
23+
inputs: [],
24+
outputs: [NodeConnectionTypes.Main],
25+
credentials: [{ name: 'taskadeApi', required: true }],
26+
webhooks: [
27+
{
28+
name: 'default',
29+
httpMethod: 'POST',
30+
responseMode: 'onReceived',
31+
path: 'webhook',
32+
},
33+
],
34+
properties: [
35+
{
36+
displayName: 'Event',
37+
name: 'event',
38+
type: 'options',
39+
options: [
40+
{ name: 'Comment Created', value: 'comment.created' },
41+
{ name: 'Member Joined Project', value: 'project.joined' },
42+
{ name: 'Project Assigned', value: 'project.assigned' },
43+
{ name: 'Project Created', value: 'project.created' },
44+
{ name: 'Task Assigned', value: 'task.assigned' },
45+
{ name: 'Task Due', value: 'task.due' },
46+
],
47+
default: 'task.due',
48+
required: true,
49+
description: 'The Taskade event that starts the workflow',
50+
},
51+
],
52+
};
53+
54+
webhookMethods = {
55+
default: {
56+
async checkExists(this: IHookFunctions): Promise<boolean> {
57+
return this.getWorkflowStaticData('node').hookId != null;
58+
},
59+
async create(this: IHookFunctions): Promise<boolean> {
60+
const response = await taskadeApiRequest.call(
61+
this as never,
62+
'POST',
63+
'/subscribeWebhook',
64+
{
65+
targetUrl: this.getNodeWebhookUrl('default') as string,
66+
triggerType: this.getNodeParameter('event') as string,
67+
},
68+
'v2',
69+
);
70+
const hookId = (response as IDataObject).hookId;
71+
if (hookId == null) {
72+
return false;
73+
}
74+
this.getWorkflowStaticData('node').hookId = hookId;
75+
return true;
76+
},
77+
async delete(this: IHookFunctions): Promise<boolean> {
78+
const staticData = this.getWorkflowStaticData('node');
79+
if (staticData.hookId == null) {
80+
return true;
81+
}
82+
try {
83+
await taskadeApiRequest.call(
84+
this as never,
85+
'POST',
86+
'/unsubscribeWebhook',
87+
{ hookId: staticData.hookId as string },
88+
'v2',
89+
);
90+
} catch {
91+
// Hook may already be gone server-side; always clear local state so
92+
// a future activation re-subscribes instead of silently never firing.
93+
}
94+
delete staticData.hookId;
95+
return true;
96+
},
97+
},
98+
};
99+
100+
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
101+
const body = this.getBodyData();
102+
return {
103+
workflowData: [this.helpers.returnJsonArray(body as IDataObject)],
104+
};
105+
}
106+
}

packages/n8n-nodes-taskade/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "n8n-nodes-taskade",
3-
"version": "0.1.0",
3+
"version": "0.2.0",
44
"description": "n8n community node for Taskade — tasks, projects, and AI agents on the Taskade public API",
55
"keywords": [
66
"n8n-community-node-package",
@@ -38,7 +38,8 @@
3838
"dist/credentials/TaskadeApi.credentials.js"
3939
],
4040
"nodes": [
41-
"dist/nodes/Taskade/Taskade.node.js"
41+
"dist/nodes/Taskade/Taskade.node.js",
42+
"dist/nodes/Taskade/TaskadeTrigger.node.js"
4243
]
4344
},
4445
"devDependencies": {

src/creates/addAgentKnowledge.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { Bundle, ZObject } from 'zapier-platform-core';
2+
3+
import { ApiResponse, parseOk, request } from '../client';
4+
import { projectField, spaceField } from '../fields';
5+
6+
const perform = async (z: ZObject, bundle: Bundle) => {
7+
const isMedia = bundle.inputData.knowledge_type === 'media';
8+
9+
const sourceId = isMedia ? bundle.inputData.media_id : bundle.inputData.project_id;
10+
if (sourceId == null || sourceId === '') {
11+
throw new z.errors.Error(
12+
isMedia ? 'Media ID is required when the source is Media File.' : 'Project is required when the source is Project.',
13+
'invalid_input',
14+
400,
15+
);
16+
}
17+
18+
const response = await request(z, bundle, {
19+
method: 'POST',
20+
path: `/agents/${bundle.inputData.agent_id}/knowledge/${isMedia ? 'media' : 'project'}`,
21+
body: isMedia
22+
? { mediaId: bundle.inputData.media_id as string }
23+
: { projectId: bundle.inputData.project_id as string },
24+
});
25+
26+
const data: ApiResponse = parseOk(z, response.json);
27+
28+
return data.item ?? { agentId: bundle.inputData.agent_id, added: true };
29+
};
30+
31+
export default {
32+
key: 'add_agent_knowledge',
33+
noun: 'Agent Knowledge',
34+
display: {
35+
label: 'Add Knowledge to AI Agent',
36+
description: 'Adds a project or media file to an AI agent’s knowledge.',
37+
hidden: false,
38+
},
39+
operation: {
40+
inputFields: [
41+
{ ...spaceField(true), label: 'Folder', altersDynamicFields: true },
42+
{
43+
key: 'agent_id',
44+
label: 'Agent',
45+
type: 'string',
46+
dynamic: 'get_all_agents.id.title',
47+
required: true,
48+
list: false,
49+
altersDynamicFields: false,
50+
},
51+
{
52+
key: 'knowledge_type',
53+
label: 'Knowledge Source',
54+
type: 'string',
55+
choices: { project: 'Project', media: 'Media File' },
56+
default: 'project',
57+
required: true,
58+
altersDynamicFields: true,
59+
},
60+
{ ...projectField(false), helpText: 'The project to add (when source is Project).' },
61+
{
62+
key: 'media_id',
63+
label: 'Media ID',
64+
type: 'string',
65+
required: false,
66+
helpText: 'The media file ID to add (when source is Media File).',
67+
},
68+
],
69+
perform,
70+
sample: { agentId: 'agent-123', added: true },
71+
outputFields: [{ key: 'agentId' }, { key: 'added', type: 'boolean' }],
72+
},
73+
};

src/creates/assignTask.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Bundle, ZObject } from 'zapier-platform-core';
2+
3+
import { ApiResponse, parseOk, request } from '../client';
4+
import { projectField, spaceField, taskField } from '../fields';
5+
6+
const perform = async (z: ZObject, bundle: Bundle) => {
7+
const raw = bundle.inputData.member_ids;
8+
const handles = (Array.isArray(raw) ? raw : [raw]).filter(Boolean);
9+
10+
const response = await request(z, bundle, {
11+
method: 'PUT',
12+
path: `/projects/${bundle.inputData.project_id}/tasks/${bundle.inputData.task_id}/assignees`,
13+
body: { handles },
14+
});
15+
16+
const data: ApiResponse = parseOk(z, response.json);
17+
18+
return data.item ?? { id: bundle.inputData.task_id, assignees: handles };
19+
};
20+
21+
export default {
22+
key: 'assign_task',
23+
noun: 'Task Assignment',
24+
display: {
25+
label: 'Assign Task',
26+
description: 'Sets the assignees of an existing task.',
27+
hidden: false,
28+
},
29+
operation: {
30+
inputFields: [
31+
spaceField(false),
32+
projectField(true),
33+
taskField('task_id', 'Task', true),
34+
{
35+
key: 'member_ids',
36+
label: 'Assign To',
37+
type: 'string',
38+
dynamic: 'get_all_assignable_members.id.displayName',
39+
required: true,
40+
list: true,
41+
altersDynamicFields: false,
42+
},
43+
],
44+
perform,
45+
sample: { id: '099630d4-267e-4b22-894b-08b69f3a4d79', assignees: ['janedoe'] },
46+
outputFields: [{ key: 'id', label: 'Task ID' }],
47+
},
48+
};

src/creates/completeProject.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { Bundle, ZObject } from 'zapier-platform-core';
2+
3+
import { ApiResponse, parseOk, request } from '../client';
4+
import { projectField, spaceField } from '../fields';
5+
6+
const perform = async (z: ZObject, bundle: Bundle) => {
7+
const completed = bundle.inputData.completed;
8+
const action = completed === false || completed === 'false' ? 'restore' : 'complete';
9+
10+
const response = await request(z, bundle, {
11+
method: 'POST',
12+
path: `/projects/${bundle.inputData.project_id}/${action}`,
13+
body: {},
14+
});
15+
16+
const data: ApiResponse = parseOk(z, response.json);
17+
18+
return data.item ?? { id: bundle.inputData.project_id, completed: action === 'complete' };
19+
};
20+
21+
export default {
22+
key: 'complete_project',
23+
noun: 'Project',
24+
display: {
25+
label: 'Complete Project',
26+
description: 'Marks a project as completed (archived), or restores it.',
27+
hidden: false,
28+
},
29+
operation: {
30+
inputFields: [
31+
spaceField(false),
32+
projectField(true),
33+
{
34+
key: 'completed',
35+
label: 'Mark as Complete',
36+
type: 'boolean',
37+
default: 'true',
38+
required: false,
39+
helpText: 'Set to No to restore a completed project instead.',
40+
},
41+
],
42+
perform,
43+
sample: { id: 'A1b2C3d4E5f6G7h8', completed: true },
44+
outputFields: [
45+
{ key: 'id', label: 'Project ID' },
46+
{ key: 'completed', type: 'boolean' },
47+
],
48+
},
49+
};

src/creates/copyProject.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { Bundle, ZObject } from 'zapier-platform-core';
2+
3+
import { ApiResponse, parseOk, request } from '../client';
4+
import { projectField, spaceField } from '../fields';
5+
6+
const perform = async (z: ZObject, bundle: Bundle) => {
7+
const body: Record<string, unknown> = { folderId: bundle.inputData.target_folder_id };
8+
if (bundle.inputData.project_title != null && bundle.inputData.project_title !== '') {
9+
body.projectTitle = bundle.inputData.project_title;
10+
}
11+
12+
const response = await request(z, bundle, {
13+
method: 'POST',
14+
path: `/projects/${bundle.inputData.project_id}/copy`,
15+
body,
16+
});
17+
18+
const data: ApiResponse = parseOk(z, response.json);
19+
20+
return data.item ?? data;
21+
};
22+
23+
export default {
24+
key: 'copy_project',
25+
noun: 'Project',
26+
display: {
27+
label: 'Copy Project',
28+
description: 'Copies a project into a folder, optionally with a new title.',
29+
hidden: false,
30+
},
31+
operation: {
32+
inputFields: [
33+
spaceField(false),
34+
projectField(true),
35+
{
36+
key: 'target_folder_id',
37+
label: 'Destination Folder',
38+
type: 'string',
39+
dynamic: 'get_all_spaces.id.name',
40+
required: true,
41+
list: false,
42+
altersDynamicFields: false,
43+
},
44+
{
45+
key: 'project_title',
46+
label: 'New Title',
47+
type: 'string',
48+
required: false,
49+
helpText: 'Optional title for the copy. Defaults to the original name.',
50+
},
51+
],
52+
perform,
53+
sample: { id: 'Z9y8X7w6V5u4T3s2', name: 'Q3 Launch Plan (Copy)' },
54+
outputFields: [
55+
{ key: 'id', label: 'Project ID' },
56+
{ key: 'name', label: 'Name' },
57+
],
58+
},
59+
};

0 commit comments

Comments
 (0)