Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "taskade",
"version": "1.1.0",
"version": "1.2.0",
"description": "Taskade is a collaboration platform for remote teams to organize and manage projects.",
"main": "index.js",
"scripts": {
Expand Down
106 changes: 106 additions & 0 deletions packages/n8n-nodes-taskade/nodes/Taskade/TaskadeTrigger.node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import type {
IDataObject,
IHookFunctions,
INodeType,
INodeTypeDescription,
IWebhookFunctions,
IWebhookResponseData,
} from 'n8n-workflow';
import { NodeConnectionTypes } from 'n8n-workflow';

import { taskadeApiRequest } from './GenericFunctions';

export class TaskadeTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'Taskade Trigger',
name: 'taskadeTrigger',
icon: 'file:taskade.svg',
group: ['trigger'],
version: 1,
subtitle: '={{$parameter["event"]}}',
description: 'Starts a workflow when something happens in Taskade',
defaults: { name: 'Taskade Trigger' },
inputs: [],
outputs: [NodeConnectionTypes.Main],
credentials: [{ name: 'taskadeApi', required: true }],
webhooks: [
{
name: 'default',
httpMethod: 'POST',
responseMode: 'onReceived',
path: 'webhook',
},
],
properties: [
{
displayName: 'Event',
name: 'event',
type: 'options',
options: [
{ name: 'Comment Created', value: 'comment.created' },
{ name: 'Member Joined Project', value: 'project.joined' },
{ name: 'Project Assigned', value: 'project.assigned' },
{ name: 'Project Created', value: 'project.created' },
{ name: 'Task Assigned', value: 'task.assigned' },
{ name: 'Task Due', value: 'task.due' },
],
default: 'task.due',
required: true,
description: 'The Taskade event that starts the workflow',
},
],
};

webhookMethods = {
default: {
async checkExists(this: IHookFunctions): Promise<boolean> {
return this.getWorkflowStaticData('node').hookId != null;
},
async create(this: IHookFunctions): Promise<boolean> {
const response = await taskadeApiRequest.call(
this as never,
'POST',
'/subscribeWebhook',
{
targetUrl: this.getNodeWebhookUrl('default') as string,
triggerType: this.getNodeParameter('event') as string,
},
'v2',
);
const hookId = (response as IDataObject).hookId;
if (hookId == null) {
return false;
}
this.getWorkflowStaticData('node').hookId = hookId;
return true;
},
async delete(this: IHookFunctions): Promise<boolean> {
const staticData = this.getWorkflowStaticData('node');
if (staticData.hookId == null) {
return true;
}
try {
await taskadeApiRequest.call(
this as never,
'POST',
'/unsubscribeWebhook',
{ hookId: staticData.hookId as string },
'v2',
);
} catch {
// Hook may already be gone server-side; always clear local state so
// a future activation re-subscribes instead of silently never firing.
}
delete staticData.hookId;
return true;
},
},
};

async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
const body = this.getBodyData();
return {
workflowData: [this.helpers.returnJsonArray(body as IDataObject)],
};
}
}
5 changes: 3 additions & 2 deletions packages/n8n-nodes-taskade/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "n8n-nodes-taskade",
"version": "0.1.0",
"version": "0.2.0",
"description": "n8n community node for Taskade — tasks, projects, and AI agents on the Taskade public API",
"keywords": [
"n8n-community-node-package",
Expand Down Expand Up @@ -38,7 +38,8 @@
"dist/credentials/TaskadeApi.credentials.js"
],
"nodes": [
"dist/nodes/Taskade/Taskade.node.js"
"dist/nodes/Taskade/Taskade.node.js",
"dist/nodes/Taskade/TaskadeTrigger.node.js"
]
},
"devDependencies": {
Expand Down
73 changes: 73 additions & 0 deletions src/creates/addAgentKnowledge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Bundle, ZObject } from 'zapier-platform-core';

import { ApiResponse, parseOk, request } from '../client';
import { projectField, spaceField } from '../fields';

const perform = async (z: ZObject, bundle: Bundle) => {
const isMedia = bundle.inputData.knowledge_type === 'media';

const sourceId = isMedia ? bundle.inputData.media_id : bundle.inputData.project_id;
if (sourceId == null || sourceId === '') {
throw new z.errors.Error(
isMedia ? 'Media ID is required when the source is Media File.' : 'Project is required when the source is Project.',
'invalid_input',
400,
);
}

const response = await request(z, bundle, {
method: 'POST',
path: `/agents/${bundle.inputData.agent_id}/knowledge/${isMedia ? 'media' : 'project'}`,
body: isMedia
? { mediaId: bundle.inputData.media_id as string }
: { projectId: bundle.inputData.project_id as string },
});

const data: ApiResponse = parseOk(z, response.json);

return data.item ?? { agentId: bundle.inputData.agent_id, added: true };
};

export default {
key: 'add_agent_knowledge',
noun: 'Agent Knowledge',
display: {
label: 'Add Knowledge to AI Agent',
description: 'Adds a project or media file to an AI agent’s knowledge.',
hidden: false,
},
operation: {
inputFields: [
{ ...spaceField(true), label: 'Folder', altersDynamicFields: true },
{
key: 'agent_id',
label: 'Agent',
type: 'string',
dynamic: 'get_all_agents.id.title',
required: true,
list: false,
altersDynamicFields: false,
},
{
key: 'knowledge_type',
label: 'Knowledge Source',
type: 'string',
choices: { project: 'Project', media: 'Media File' },
default: 'project',
required: true,
altersDynamicFields: true,
},
{ ...projectField(false), helpText: 'The project to add (when source is Project).' },
{
key: 'media_id',
label: 'Media ID',
type: 'string',
required: false,
helpText: 'The media file ID to add (when source is Media File).',
},
],
perform,
sample: { agentId: 'agent-123', added: true },
outputFields: [{ key: 'agentId' }, { key: 'added', type: 'boolean' }],
},
};
48 changes: 48 additions & 0 deletions src/creates/assignTask.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Bundle, ZObject } from 'zapier-platform-core';

import { ApiResponse, parseOk, request } from '../client';
import { projectField, spaceField, taskField } from '../fields';

const perform = async (z: ZObject, bundle: Bundle) => {
const raw = bundle.inputData.member_ids;
const handles = (Array.isArray(raw) ? raw : [raw]).filter(Boolean);

const response = await request(z, bundle, {
method: 'PUT',
path: `/projects/${bundle.inputData.project_id}/tasks/${bundle.inputData.task_id}/assignees`,
body: { handles },
});

const data: ApiResponse = parseOk(z, response.json);

return data.item ?? { id: bundle.inputData.task_id, assignees: handles };
};

export default {
key: 'assign_task',
noun: 'Task Assignment',
display: {
label: 'Assign Task',
description: 'Sets the assignees of an existing task.',
hidden: false,
},
operation: {
inputFields: [
spaceField(false),
projectField(true),
taskField('task_id', 'Task', true),
{
key: 'member_ids',
label: 'Assign To',
type: 'string',
dynamic: 'get_all_assignable_members.id.displayName',
required: true,
list: true,
altersDynamicFields: false,
},
],
perform,
sample: { id: '099630d4-267e-4b22-894b-08b69f3a4d79', assignees: ['janedoe'] },
outputFields: [{ key: 'id', label: 'Task ID' }],
},
};
49 changes: 49 additions & 0 deletions src/creates/completeProject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Bundle, ZObject } from 'zapier-platform-core';

import { ApiResponse, parseOk, request } from '../client';
import { projectField, spaceField } from '../fields';

const perform = async (z: ZObject, bundle: Bundle) => {
const completed = bundle.inputData.completed;
const action = completed === false || completed === 'false' ? 'restore' : 'complete';

const response = await request(z, bundle, {
method: 'POST',
path: `/projects/${bundle.inputData.project_id}/${action}`,
body: {},
});

const data: ApiResponse = parseOk(z, response.json);

return data.item ?? { id: bundle.inputData.project_id, completed: action === 'complete' };
};

export default {
key: 'complete_project',
noun: 'Project',
display: {
label: 'Complete Project',
description: 'Marks a project as completed (archived), or restores it.',
hidden: false,
},
operation: {
inputFields: [
spaceField(false),
projectField(true),
{
key: 'completed',
label: 'Mark as Complete',
type: 'boolean',
default: 'true',
required: false,
helpText: 'Set to No to restore a completed project instead.',
},
],
perform,
sample: { id: 'A1b2C3d4E5f6G7h8', completed: true },
outputFields: [
{ key: 'id', label: 'Project ID' },
{ key: 'completed', type: 'boolean' },
],
},
};
59 changes: 59 additions & 0 deletions src/creates/copyProject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Bundle, ZObject } from 'zapier-platform-core';

import { ApiResponse, parseOk, request } from '../client';
import { projectField, spaceField } from '../fields';

const perform = async (z: ZObject, bundle: Bundle) => {
const body: Record<string, unknown> = { folderId: bundle.inputData.target_folder_id };
if (bundle.inputData.project_title != null && bundle.inputData.project_title !== '') {
body.projectTitle = bundle.inputData.project_title;
}

const response = await request(z, bundle, {
method: 'POST',
path: `/projects/${bundle.inputData.project_id}/copy`,
body,
});

const data: ApiResponse = parseOk(z, response.json);

return data.item ?? data;
};

export default {
key: 'copy_project',
noun: 'Project',
display: {
label: 'Copy Project',
description: 'Copies a project into a folder, optionally with a new title.',
hidden: false,
},
operation: {
inputFields: [
spaceField(false),
projectField(true),
{
key: 'target_folder_id',
label: 'Destination Folder',
type: 'string',
dynamic: 'get_all_spaces.id.name',
required: true,
list: false,
altersDynamicFields: false,
},
{
key: 'project_title',
label: 'New Title',
type: 'string',
required: false,
helpText: 'Optional title for the copy. Defaults to the original name.',
},
],
perform,
sample: { id: 'Z9y8X7w6V5u4T3s2', name: 'Q3 Launch Plan (Copy)' },
outputFields: [
{ key: 'id', label: 'Project ID' },
{ key: 'name', label: 'Name' },
],
},
};
Loading
Loading