This workflow corresponds to n8n.io template #omniproject-core-sync — we link there as the canonical source.
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": "OmniProject Core Sync",
"meta": {
"templateId": "omniproject-core-sync",
"description": "Reference workflow implementing the OmniProject gateway contract: routes X-OmniProject-Action to Plane / OpenProject and returns a normalized N8nActionResult."
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "omniproject",
"responseMode": "responseNode",
"options": {}
},
"id": "11111111-1111-4111-8111-111111111111",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
-360,
320
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"leftValue": "={{ $json.body.origin }}",
"rightValue": "={{ $json.body.payload && $json.body.payload.lastUpdatedBy }}",
"operator": {
"type": "string",
"operation": "notEquals"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "22222222-2222-4222-8222-222222222222",
"name": "Drop Loop? (origin != lastUpdatedBy)",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
-160,
320
],
"notes": "Idempotency / loop guard. The gateway sends X-OmniProject-Idempotency-Key and origin. If an inbound change already originated from this system (origin === lastUpdatedBy), the false branch drops it to prevent webhook storms."
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"leftValue": "={{ $json.body.action }}",
"rightValue": "list_projects",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "list_projects"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"leftValue": "={{ $json.body.action }}",
"rightValue": "list_issues",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "list_issues"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"leftValue": "={{ $json.body.action }}",
"rightValue": "create_issue",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "create_issue"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"leftValue": "={{ $json.body.action }}",
"rightValue": "update_issue",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "update_issue"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"leftValue": "={{ $json.body.action }}",
"rightValue": "get_resource_capacity",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "get_resource_capacity"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"leftValue": "={{ $json.body.action }}",
"rightValue": "get_project_financials",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "get_project_financials"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"leftValue": "={{ $json.body.action }}",
"rightValue": "get_portfolio_health",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "get_portfolio_health"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "loose"
},
"conditions": [
{
"leftValue": "={{ $json.body.action }}",
"rightValue": "get_capabilities",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "get_capabilities"
}
]
},
"options": {
"fallbackOutput": "extra"
}
},
"id": "33333333-3333-4333-8333-333333333333",
"name": "Route Action",
"type": "n8n-nodes-base.switch",
"typeVersion": 3,
"position": [
80,
240
]
},
{
"parameters": {
"method": "GET",
"url": "={{ ($json.body.source === 'openproject' ? $env.OPENPROJECT_INSTANCE_URL : $env.PLANE_INSTANCE_URL) }}/api/v1/projects",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $json.body.payload.userContext.token }}"
}
]
},
"options": {}
},
"id": "44444444-4444-4444-8444-444444444444",
"name": "List Projects (Plane/OpenProject)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
340,
-120
],
"notes": "Dynamic per-user impersonation: Authorization uses the active user's OIDC token from payload.userContext.token, not a shared admin key."
},
{
"parameters": {
"method": "GET",
"url": "={{ ($json.body.source === 'openproject' ? $env.OPENPROJECT_INSTANCE_URL : $env.PLANE_INSTANCE_URL) }}/api/v1/projects/{{ $json.body.payload.projectId }}/issues",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $json.body.payload.userContext.token }}"
}
]
},
"options": {}
},
"id": "55555555-5555-4555-8555-555555555555",
"name": "List Issues",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
340,
80
]
},
{
"parameters": {
"method": "POST",
"url": "={{ ($json.body.source === 'openproject' ? $env.OPENPROJECT_INSTANCE_URL : $env.PLANE_INSTANCE_URL) }}/api/v1/projects/{{ $json.body.payload.projectId }}/issues",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $json.body.payload.userContext.token }}"
},
{
"name": "X-OmniProject-Idempotency-Key",
"value": "={{ $json.body.idempotencyKey }}"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify(Object.assign({}, $json.body.payload, { lastUpdatedBy: $json.body.origin })) }}",
"options": {}
},
"id": "66666666-6666-4666-8666-666666666666",
"name": "Create Issue",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
340,
300
],
"notes": "Writes lastUpdatedBy = origin so the loop guard can drop the echo webhook this write will trigger."
},
{
"parameters": {
"method": "PATCH",
"url": "={{ ($json.body.source === 'openproject' ? $env.OPENPROJECT_INSTANCE_URL : $env.PLANE_INSTANCE_URL) }}/api/v1/projects/{{ $json.body.payload.projectId }}/issues/{{ $json.body.payload.issueId }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $json.body.payload.userContext.token }}"
},
{
"name": "X-OmniProject-Idempotency-Key",
"value": "={{ $json.body.idempotencyKey }}"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify(Object.assign({}, $json.body.payload, { lastUpdatedBy: $json.body.origin })) }}",
"options": {}
},
"id": "77777777-7777-4777-8777-777777777777",
"name": "Update Issue",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
340,
520
]
},
{
"parameters": {
"jsCode": "// Normalize any backend response into the N8nActionResult contract\n// expected by the OmniProject gateway: { success, data, message }.\nconst action = $('Webhook').first().json.body.action;\nconst rows = items.map((i) => i.json);\nconst data = rows.length === 1 ? rows[0] : rows;\nreturn [{ json: { success: true, data, message: action + ' ok' } }];"
},
"id": "88888888-8888-4888-8888-888888888888",
"name": "Normalize \u2192 N8nActionResult",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
620,
200
]
},
{
"parameters": {
"jsCode": "const action = $('Webhook').first().json.body.action;\nreturn [{ json: { success: false, data: {}, message: 'Unsupported action: ' + action } }];"
},
"id": "99999999-9999-4999-8999-999999999999",
"name": "Unsupported Action",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
620,
480
]
},
{
"parameters": {
"jsCode": "return [{ json: { success: true, data: { dropped: true }, message: 'Loop mutation dropped (origin === lastUpdatedBy)' } }];"
},
"id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
"name": "Drop (loop)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
80,
560
]
},
{
"parameters": {
"respondWith": "firstIncomingItem",
"options": {}
},
"id": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
"name": "Respond N8nActionResult",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [
880,
320
]
},
{
"parameters": {
"method": "GET",
"url": "={{ $env.DYNAMICS_BC_URL }}/api/v2.0/companies/jobs?$filter=projectId eq '{{ $json.body.payload.projectId }}'",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $json.body.payload.userContext.token }}"
}
]
},
"options": {}
},
"id": "d1d1d1d1-d1d1-4d1d-8d1d-d1d1d1d1d1d1",
"name": "Resource Capacity (Enterprise: Dynamics 365 BC Jobs / OSS: TaskJuggler)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
340,
740
],
"notes": "ENTERPRISE: Microsoft Dynamics 365 Business Central Job Ledger (/api/v2.0/companies/jobs). OSS: replace with TaskJuggler resource report or native worker-assignment tables. Map \u2192 ResourceCapacity[] (resourceId, resourceName, role, allocationPercentage, assignedHours, availableHours, utilizationState)."
},
{
"parameters": {
"method": "GET",
"url": "={{ $env.SAP_S4_URL }}/sap/opu/odata/sap/API_ENTERPRISE_PROJECT_SRV/A_EnterpriseProject('{{ $json.body.payload.projectId }}')",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $json.body.payload.userContext.token }}"
}
]
},
"options": {}
},
"id": "d2d2d2d2-d2d2-4d2d-8d2d-d2d2d2d2d2d2",
"name": "Project Financials (Enterprise: SAP S/4HANA / OSS: Dolibarr|Odoo)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
340,
940
],
"notes": "ENTERPRISE: SAP S/4HANA Project API (OData). OSS: pull budget line items from Dolibarr ERP or Odoo Community tables. Compute EVM and map \u2192 ProjectFinancials (currency, budgetAllocated, actualBurn, earnedValue, cpi, spi, financialHealth, forecastCostAtCompletion)."
},
{
"parameters": {
"method": "GET",
"url": "={{ $env.OPENPROJECT_INSTANCE_URL }}/api/v3/projects",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $json.body.payload.userContext.token }}"
}
]
},
"options": {}
},
"id": "d3d3d3d3-d3d3-4d3d-8d3d-d3d3d3d3d3d3",
"name": "Portfolio Health (OSS: OpenProject PPM / Enterprise: SAP|Dynamics rollup)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
340,
1140
],
"notes": "OSS: OpenProject high-level PPM project health records (/api/v3/projects). ENTERPRISE: aggregate from SAP S/4HANA / Dynamics 365 rollups. Map \u2192 PortfolioHealthSummary[] (projectId, projectName, ragStatus, scheduleVarianceDays, budgetVariancePercentage, activeBlockersCount)."
},
{
"parameters": {
"jsCode": "// Declare which data domains THIS deployment's backends can populate.\n// Edit to match what you've wired (resources/financials need a capacity / ERP\n// source; history needs backend journals/changelog; raid needs a risk\n// register). The gateway exposes this at GET /api/capabilities so the UI labels\n// available reports/views.\nreturn [{ json: { success: true, data: {\n issues: true,\n scheduling: true,\n resources: false,\n financials: false,\n portfolio: true,\n baseline: false,\n blockers: false,\n history: false,\n raid: false\n} } }];"
},
"id": "e1e1e1e1-e1e1-4e1e-8e1e-e1e1e1e1e1e1",
"name": "Capabilities (edit me)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
340,
1340
]
},
{
"parameters": {
"content": "## Dual routing: Open-Source vs Enterprise\n\nEach analytics action has an OSS and an Enterprise mapping \u2014 pick one HTTP node per action and delete the other, then normalize to the Part 1 schema.\n\n| Action | Open-source | Enterprise |\n|---|---|---|\n| get_resource_capacity | TaskJuggler / worker tables | Dynamics 365 BC Jobs |\n| get_project_financials | Dolibarr / Odoo CE | SAP S/4HANA Project API |\n| get_portfolio_health | OpenProject PPM | SAP / Dynamics rollup |\n\nAll variants MUST return the exact `ResourceCapacity` / `ProjectFinancials` / `PortfolioHealthSummary` shapes before the Normalize node.",
"height": 300,
"width": 460
},
"id": "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
"name": "Dual Routing Notes",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-360,
220
]
},
{
"parameters": {
"content": "## OmniProject Core Sync\n\nImplements the gateway contract:\n- **Webhook** receives `POST` with body `{ action, payload, source, origin, idempotencyKey }` and headers `X-OmniProject-Action / -Source / -Origin / -Idempotency-Key`.\n- **Loop guard** drops echoes where `origin === payload.lastUpdatedBy`.\n- **Route Action** switches on `list_projects | list_issues | create_issue | update_issue`.\n- HTTP nodes call Plane (`PLANE_INSTANCE_URL`) or OpenProject (`OPENPROJECT_INSTANCE_URL`) and authenticate **as the active user** via `payload.userContext.token`.\n- **Normalize** returns `{ success, data, message }` (N8nActionResult).\n\nSet env vars `PLANE_INSTANCE_URL` and `OPENPROJECT_INSTANCE_URL` in n8n. Adjust the API paths to your backend versions.",
"height": 320,
"width": 420
},
"id": "cccccccc-cccc-4ccc-8ccc-cccccccccccc",
"name": "Notes",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-360,
-160
]
}
],
"connections": {
"Webhook": {
"main": [
[
{
"node": "Drop Loop? (origin != lastUpdatedBy)",
"type": "main",
"index": 0
}
]
]
},
"Drop Loop? (origin != lastUpdatedBy)": {
"main": [
[
{
"node": "Route Action",
"type": "main",
"index": 0
}
],
[
{
"node": "Drop (loop)",
"type": "main",
"index": 0
}
]
]
},
"Route Action": {
"main": [
[
{
"node": "List Projects (Plane/OpenProject)",
"type": "main",
"index": 0
}
],
[
{
"node": "List Issues",
"type": "main",
"index": 0
}
],
[
{
"node": "Create Issue",
"type": "main",
"index": 0
}
],
[
{
"node": "Update Issue",
"type": "main",
"index": 0
}
],
[
{
"node": "Resource Capacity (Enterprise: Dynamics 365 BC Jobs / OSS: TaskJuggler)",
"type": "main",
"index": 0
}
],
[
{
"node": "Project Financials (Enterprise: SAP S/4HANA / OSS: Dolibarr|Odoo)",
"type": "main",
"index": 0
}
],
[
{
"node": "Portfolio Health (OSS: OpenProject PPM / Enterprise: SAP|Dynamics rollup)",
"type": "main",
"index": 0
}
],
[
{
"node": "Capabilities (edit me)",
"type": "main",
"index": 0
}
],
[
{
"node": "Unsupported Action",
"type": "main",
"index": 0
}
]
]
},
"Capabilities (edit me)": {
"main": [
[
{
"node": "Respond N8nActionResult",
"type": "main",
"index": 0
}
]
]
},
"Resource Capacity (Enterprise: Dynamics 365 BC Jobs / OSS: TaskJuggler)": {
"main": [
[
{
"node": "Normalize \u2192 N8nActionResult",
"type": "main",
"index": 0
}
]
]
},
"Project Financials (Enterprise: SAP S/4HANA / OSS: Dolibarr|Odoo)": {
"main": [
[
{
"node": "Normalize \u2192 N8nActionResult",
"type": "main",
"index": 0
}
]
]
},
"Portfolio Health (OSS: OpenProject PPM / Enterprise: SAP|Dynamics rollup)": {
"main": [
[
{
"node": "Normalize \u2192 N8nActionResult",
"type": "main",
"index": 0
}
]
]
},
"List Projects (Plane/OpenProject)": {
"main": [
[
{
"node": "Normalize \u2192 N8nActionResult",
"type": "main",
"index": 0
}
]
]
},
"List Issues": {
"main": [
[
{
"node": "Normalize \u2192 N8nActionResult",
"type": "main",
"index": 0
}
]
]
},
"Create Issue": {
"main": [
[
{
"node": "Normalize \u2192 N8nActionResult",
"type": "main",
"index": 0
}
]
]
},
"Update Issue": {
"main": [
[
{
"node": "Normalize \u2192 N8nActionResult",
"type": "main",
"index": 0
}
]
]
},
"Normalize \u2192 N8nActionResult": {
"main": [
[
{
"node": "Respond N8nActionResult",
"type": "main",
"index": 0
}
]
]
},
"Unsupported Action": {
"main": [
[
{
"node": "Respond N8nActionResult",
"type": "main",
"index": 0
}
]
]
},
"Drop (loop)": {
"main": [
[
{
"node": "Respond N8nActionResult",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
OmniProject Core Sync. Uses httpRequest. Webhook trigger; 17 nodes.
Source: https://github.com/walshd1/Omniproject/blob/main/artifacts/n8n-blueprints/omniproject-core-sync.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.
This n8n template provides enterprise-level version control for your workflows using GitHub integration. Stop losing hours to broken workflows and manual exports – get proper commit history, visual di
This flow creates dummy files for every item added in your *Arrs (Radarr/Sonarr) with the tag .
eek-Go v2 (Batch-Then-Review). Uses httpRequest. Webhook trigger; 75 nodes.
This workflow receives webhook requests from a content calendar and uses the X API v2 to publish text posts, threads, image/video posts, and polls, as well as delete existing posts and run a credentia
This workflow acts as a central API gateway for all technical indicator agents in the Binance Spot Market Quant AI system. It listens for incoming webhook requests and dynamically routes them to the c