The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"name": "21 - TOOL - create_task",
"nodes": [
{
"parameters": {
"content": "## Narrow create worker\nThis workflow accepts only the fields needed for one task. It validates every value before writing and uses `requestId` as an idempotency key.\n\nA repeated request returns the original row. Reusing the same request ID with different input returns a conflict instead of silently changing or duplicating data.\n\n**Phase 5:** the AI cannot call this worker directly. Only `40 - CONFIRM - Task Write` dispatches it after consuming a valid, same-session confirmation.",
"height": 370,
"width": 590,
"color": 3
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-680,
-370
],
"id": "42000000-0000-4000-8000-000000000001",
"name": "Create tool explanation"
},
{
"parameters": {
"inputSource": "workflowInputs",
"workflowInputs": {
"values": [
{
"name": "sessionId",
"type": "string"
},
{
"name": "requestId",
"type": "string"
},
{
"name": "title",
"type": "string"
},
{
"name": "description",
"type": "string"
},
{
"name": "status",
"type": "string"
},
{
"name": "priority",
"type": "string"
},
{
"name": "dueDate",
"type": "string"
}
]
}
},
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1.2,
"position": [
-540,
80
],
"id": "42000000-0000-4000-8000-000000000002",
"name": "Tool Input"
},
{
"parameters": {
"mode": "runOnceForEachItem",
"language": "javaScript",
"jsCode": "const clean = (value) => typeof value === 'string' ? value.trim() : '';\nconst sessionId = clean($json.sessionId);\nconst requestId = clean($json.requestId);\nconst title = clean($json.title);\nconst description = clean($json.description);\nconst status = clean($json.status).toLowerCase();\nconst priority = clean($json.priority).toLowerCase();\nconst dueDate = clean($json.dueDate);\nconst uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\nconst statuses = ['backlog', 'todo', 'in_progress', 'blocked', 'done'];\nconst priorities = ['low', 'medium', 'high'];\nconst proposedInput = { sessionId, requestId, title, description, status, priority, dueDate };\nlet error = null;\nlet dueDateIso = null;\n\nif (!uuidPattern.test(sessionId)) {\n error = { code: 'INVALID_SESSION', message: 'The task request needs a valid conversation session.' };\n} else if (!uuidPattern.test(requestId)) {\n error = { code: 'INVALID_REQUEST_ID', message: 'Creating a task requires a unique request ID.' };\n} else if (title.length === 0) {\n error = { code: 'EMPTY_TITLE', message: 'Task title cannot be empty.' };\n} else if (title.length > 120) {\n error = { code: 'TITLE_TOO_LONG', message: 'Task title must be 120 characters or fewer.' };\n} else if (description.length > 2000) {\n error = { code: 'DESCRIPTION_TOO_LONG', message: 'Task description must be 2,000 characters or fewer.' };\n} else if (!statuses.includes(status)) {\n error = { code: 'INVALID_STATUS', message: `Status must be one of: ${statuses.join(', ')}.` };\n} else if (!priorities.includes(priority)) {\n error = { code: 'INVALID_PRIORITY', message: `Priority must be one of: ${priorities.join(', ')}.` };\n} else if (dueDate !== '') {\n const parsed = /^\\d{4}-\\d{2}-\\d{2}$/.test(dueDate)\n ? new Date(`${dueDate}T00:00:00.000Z`)\n : new Date('invalid');\n if (Number.isNaN(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== dueDate) {\n error = { code: 'INVALID_DUE_DATE', message: 'Due date must be a real date in YYYY-MM-DD format, or empty.' };\n } else {\n dueDateIso = parsed.toISOString();\n }\n}\n\nreturn {\n json: {\n valid: error === null,\n sessionId,\n requestId,\n title,\n description,\n status,\n priority,\n dueDate,\n dueDateIso,\n proposedInput,\n ...(error ? { response: { ok: false, error } } : {})\n }\n};"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-300,
80
],
"id": "42000000-0000-4000-8000-000000000003",
"name": "Validate Create Input"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "42000000-0000-4000-8000-000000000004",
"leftValue": "={{ $json.valid }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
-60,
80
],
"id": "42000000-0000-4000-8000-000000000005",
"name": "Input Is Valid?"
},
{
"parameters": {
"resource": "row",
"operation": "get",
"dataTableId": {
"__rl": true,
"value": "tasks",
"mode": "name"
},
"matchType": "allConditions",
"filters": {
"conditions": [
{
"keyName": "requestId",
"condition": "eq",
"keyValue": "={{ $('Validate Create Input').item.json.requestId }}"
}
]
},
"returnAll": false,
"limit": 1,
"orderBy": false
},
"type": "n8n-nodes-base.dataTable",
"typeVersion": 1.1,
"position": [
200,
-20
],
"id": "42000000-0000-4000-8000-000000000006",
"name": "Find Existing Request",
"alwaysOutputData": true,
"onError": "continueRegularOutput"
},
{
"parameters": {
"mode": "runOnceForAllItems",
"language": "javaScript",
"jsCode": "const input = $('Validate Create Input').first().json;\nconst rows = $input.all().map((item) => item.json);\nconst dataError = rows.find((row) => typeof row.error === 'string' && row.error !== '');\n\nif (dataError) {\n return [{\n json: {\n ...input,\n shouldCreate: false,\n response: {\n ok: false,\n error: {\n code: 'TASK_DATA_UNAVAILABLE',\n message: 'Local task data is not ready. Run 10 - SETUP - Local Task Data, then try again.'\n }\n }\n }\n }];\n}\n\nconst existing = rows.find((row) => Number.isInteger(row.id));\nif (!existing) {\n return [{ json: { ...input, shouldCreate: true } }];\n}\n\nconst sameInput =\n String(existing.title ?? '') === input.title &&\n String(existing.description ?? '') === input.description &&\n String(existing.status ?? '') === input.status &&\n String(existing.priority ?? '') === input.priority &&\n String(existing.dueDate ?? '').slice(0, 10) === input.dueDate;\nconst task = {\n id: existing.id,\n title: existing.title,\n description: existing.description,\n status: existing.status,\n priority: existing.priority,\n dueDate: existing.dueDate ?? null,\n createdAt: existing.createdAt,\n updatedAt: existing.updatedAt\n};\n\nreturn [{\n json: {\n ...input,\n shouldCreate: false,\n response: sameInput\n ? { ok: true, created: false, idempotent: true, task }\n : {\n ok: false,\n error: {\n code: 'IDEMPOTENCY_CONFLICT',\n message: 'That request ID was already used with different task details. Start a fresh request instead.'\n }\n }\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
460,
-20
],
"id": "42000000-0000-4000-8000-000000000007",
"name": "Decide Create Action"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "42000000-0000-4000-8000-000000000008",
"leftValue": "={{ $json.shouldCreate }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
700,
-20
],
"id": "42000000-0000-4000-8000-000000000009",
"name": "Create New Row?"
},
{
"parameters": {
"resource": "row",
"operation": "insert",
"dataTableId": {
"__rl": true,
"value": "tasks",
"mode": "name"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"requestId": "={{ $json.requestId }}",
"lastRequestId": "={{ $json.requestId }}",
"title": "={{ $json.title }}",
"description": "={{ $json.description }}",
"status": "={{ $json.status }}",
"priority": "={{ $json.priority }}",
"dueDate": "={{ $json.dueDateIso }}"
},
"matchingColumns": [],
"schema": [
{
"id": "requestId",
"displayName": "requestId",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "string"
},
{
"id": "lastRequestId",
"displayName": "lastRequestId",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "string"
},
{
"id": "title",
"displayName": "title",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "string"
},
{
"id": "description",
"displayName": "description",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "string"
},
{
"id": "status",
"displayName": "status",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "string"
},
{
"id": "priority",
"displayName": "priority",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "string"
},
{
"id": "dueDate",
"displayName": "dueDate",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "date"
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {}
},
"type": "n8n-nodes-base.dataTable",
"typeVersion": 1.1,
"position": [
940,
-100
],
"id": "42000000-0000-4000-8000-000000000010",
"name": "Insert Task",
"onError": "continueRegularOutput"
},
{
"parameters": {
"mode": "runOnceForAllItems",
"language": "javaScript",
"jsCode": "const input = $('Decide Create Action').first().json;\nconst row = $input.first().json;\nif (typeof row.error === 'string' && row.error !== '') {\n return [{\n json: {\n ...input,\n response: {\n ok: false,\n error: { code: 'CREATE_FAILED', message: 'The task could not be created. Check the local task table and try again.' }\n }\n }\n }];\n}\nreturn [{\n json: {\n ...input,\n response: {\n ok: true,\n created: true,\n idempotent: false,\n task: {\n id: row.id,\n title: row.title,\n description: row.description,\n status: row.status,\n priority: row.priority,\n dueDate: row.dueDate ?? null,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt\n }\n }\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1180,
-100
],
"id": "42000000-0000-4000-8000-000000000011",
"name": "Shape Create Result"
},
{
"parameters": {
"mode": "runOnceForEachItem",
"language": "javaScript",
"jsCode": "const response = $json.response;\nconst error = response?.ok === false ? String(response.error?.message ?? 'Tool failed') : '';\nreturn {\n json: {\n occurredAt: new Date().toISOString(),\n sessionId: String($json.sessionId ?? ''),\n requestId: String($json.requestId ?? ''),\n toolName: 'create_task',\n proposedInput: JSON.stringify($json.proposedInput ?? {}),\n result: JSON.stringify(response),\n error,\n response\n }\n};"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1420,
80
],
"id": "42000000-0000-4000-8000-000000000012",
"name": "Prepare Audit"
},
{
"parameters": {
"resource": "row",
"operation": "insert",
"dataTableId": {
"__rl": true,
"value": "tool_audit",
"mode": "name"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"occurredAt": "={{ $json.occurredAt }}",
"sessionId": "={{ $json.sessionId }}",
"requestId": "={{ $json.requestId }}",
"toolName": "={{ $json.toolName }}",
"proposedInput": "={{ $json.proposedInput }}",
"result": "={{ $json.result }}",
"error": "={{ $json.error }}"
},
"matchingColumns": [],
"schema": [
{
"id": "occurredAt",
"displayName": "occurredAt",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "date"
},
{
"id": "sessionId",
"displayName": "sessionId",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "string"
},
{
"id": "requestId",
"displayName": "requestId",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "string"
},
{
"id": "toolName",
"displayName": "toolName",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "string"
},
{
"id": "proposedInput",
"displayName": "proposedInput",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "string"
},
{
"id": "result",
"displayName": "result",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "string"
},
{
"id": "error",
"displayName": "error",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "string"
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {}
},
"type": "n8n-nodes-base.dataTable",
"typeVersion": 1.1,
"position": [
1660,
80
],
"id": "42000000-0000-4000-8000-000000000013",
"name": "Write Tool Audit",
"onError": "continueRegularOutput"
},
{
"parameters": {
"mode": "runOnceForEachItem",
"language": "javaScript",
"jsCode": "const prepared = $('Prepare Audit').item.json;\nconst response = { ...prepared.response };\nif (!Number.isInteger($json.id)) {\n response.auditWarning = 'The tool result could not be written to the local audit table.';\n}\nreturn { json: response };"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1900,
80
],
"id": "42000000-0000-4000-8000-000000000014",
"name": "Return Tool Result"
}
],
"connections": {
"Tool Input": {
"main": [
[
{
"node": "Validate Create Input",
"type": "main",
"index": 0
}
]
]
},
"Validate Create Input": {
"main": [
[
{
"node": "Input Is Valid?",
"type": "main",
"index": 0
}
]
]
},
"Input Is Valid?": {
"main": [
[
{
"node": "Find Existing Request",
"type": "main",
"index": 0
}
],
[
{
"node": "Prepare Audit",
"type": "main",
"index": 0
}
]
]
},
"Find Existing Request": {
"main": [
[
{
"node": "Decide Create Action",
"type": "main",
"index": 0
}
]
]
},
"Decide Create Action": {
"main": [
[
{
"node": "Create New Row?",
"type": "main",
"index": 0
}
]
]
},
"Create New Row?": {
"main": [
[
{
"node": "Insert Task",
"type": "main",
"index": 0
}
],
[
{
"node": "Prepare Audit",
"type": "main",
"index": 0
}
]
]
},
"Insert Task": {
"main": [
[
{
"node": "Shape Create Result",
"type": "main",
"index": 0
}
]
]
},
"Shape Create Result": {
"main": [
[
{
"node": "Prepare Audit",
"type": "main",
"index": 0
}
]
]
},
"Prepare Audit": {
"main": [
[
{
"node": "Write Tool Audit",
"type": "main",
"index": 0
}
]
]
},
"Write Tool Audit": {
"main": [
[
{
"node": "Return Tool Result",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1",
"executionTimeout": 20,
"saveDataErrorExecution": "all",
"saveDataSuccessExecution": "all",
"saveManualExecutions": true
},
"versionId": "42000000-0000-4000-8000-000000000099",
"meta": {
"phase": 5,
"testedWithN8n": "2.30.5",
"toolRisk": "write",
"agentConnection": "confirmation-executor-only"
},
"id": "phase4CreateTask",
"tags": []
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
21 - TOOL - create_task. Uses executeWorkflowTrigger, dataTable. Event-driven trigger; 12 nodes.
Source: https://github.com/drsamdonegan/ai-solopreneur/blob/main/n8n/workflows/21-tool-create-task.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Reagendamiento. Uses executeWorkflowTrigger, redis, n8n-nodes-evolution-api, dataTable. Event-driven trigger; 73 nodes.
Agendamiento. Uses n8n-nodes-evolution-api, redis, dataTable, executeWorkflowTrigger. Event-driven trigger; 60 nodes.
Cancelacion. Uses executeWorkflowTrigger, redis, n8n-nodes-evolution-api, dataTable. Event-driven trigger; 36 nodes.
40 - CONFIRM - Task Write. Uses executeWorkflowTrigger, dataTable. Event-driven trigger; 19 nodes.
This workflow provides a reusable error handling, audit logging, and observability pattern for n8n workflows using two n8n custom Data Tables: and .