Toggl Integration [GOT CREATED]

Ultimately replicating setup described here: Time pressure. How tracking a few key metrics can have… | by Vladyslav Sitalo | Medium
In more unified/coherent fashion :slight_smile:

For that the Toggl integration should provide a way to react to the new time entries.

@stvad Thanks for clarifying. I have a couple of integration to finish and then will check it out.

1 Like

Awesome, thank you @RicardoE105!

1 Like

@stvad Looks like Toggl does not have webhooks, the alternative option would be polling every x minutes, but I don’t know if we want to go that route. What do you think? @jan

@RicardoE105 yeah. As a food for thought - I believe there are more services out there that provide an API for querying things then services that provide webhooks. So you probably eventually would want to implement nodes that do the polling anyway.

Yes, I agree. At some point, we will probably not get around polling. Like you said do some services sadly not offer webhooks. Not sure though if n8n should provide some abstraction layer for it or if the nodes should simply implement it themself with an interval/cron.

Here a previous discussion about polling:

As you mention in one of the replies there - getting a stream of events from polling without duplicates and without skipping the events reliably is non-trivial, so some abstraction layer/support from n8n would come in handy IMO

I’m curious what you’ve decided on this one @RicardoE105 @jan?

That it will get implemented but is sadly not a top priority right now. As the resources are very scarce I have to concentrate on things which make the biggest bang for the buck.

This will hopefully change soon and then this will get implemented.

2 Likes

Just released [email protected] with the Toggle-Trigger which @RicardoE105 created.
Happy new year 2020!

Awesome! Happy new year to you as well!

2 Likes

This Toggl integration looks like just the thing for my use case: I want to poll Toggl at the end of the day, look for new time entries, and log those into InvoiceNinja. Thanks for the work you did to create this node.

There is one piece in this workflow that I cannot figure out. I can get the time entry information from Toggl, but the only way to link that to a project in the current node appears to be the pid. I can’t find a way to connect that to a Project Name so I can match it to a Project Name in InvoiceNinja.

Would it be possible either to add Project Name to the Toggl Trigger or to build a general Toggl Node that I could use to input the pid and output the Project Name?

Alternatively, is there another utility node that I might be able to use to make that connection? (I’m still getting my bearings with n8n.)

Thank you for your time.

The Toggl Trigger definitely needs some love.

Right off the top of my head, I would say you can achieve this with the Cron node, HTTP node, Merge node, If node, and the Invoice Ninja node.

The Cron node will kick off the workflow once a day.

Then, the HTTP nodes will get the info needed using the following endpoints:

  1. Get the time-entries in a date range. Here.
  2. Get the projects. Here.

Then, you use the Merge node, specifically, the merge by key operation, to merge both outputs by the PID (Project ID).

Finally, add the time entries to Invoice Ninja.

Of course, having a regular node for Toggl would make the workflow much simpler. In case you are still interested you can always make a feature request.

Ooh, I like this! In the long run, I’ll be a lot better off getting to know the utility nodes. Thanks for pointing me in the right direction.

Hey! We’re you able to get a working workflow with this? If so, could you share it?

I kind of did. This was my first real attempt at an n8n integration, and it feels a bit convoluted to me.

A couple of caveats, one of them big:

It depends on a cron task to activate the webhook. The n8n execution logs show that it’s firing properly every night. But I just looked in InvoiceNinja, and it hadn’t logged anything since mid-April. So there’s a breakdown somewhere, I’m just not sure where.

I just ran an every-minute cron to fire the webhook, and it worked properly. So if I had to guess, it has something to do with when the cron is firing.

I did my best to configure it so that the cron fires at the end of the day and that it only grabs the time entries for the previous 24 hours. But I couldn’t quite wrap my head around the timezones. After some trial and error, I thought I had it working (the logs show it activating at 23:46). I’ll dive back in when I have some time.

Hopefully this is helpful to you. I’ve indicated places in the JSON where you’ll need to add some API auth or workspace-specific numbers. You can search for “ADD_YOUR” to find them all.

Let me know if you or anyone else finds a way to improve it.

{
  "name": "Toggl Time -> Invoice Ninja Tasks",
  "nodes": [
    {
      "parameters": {},
      "name": "Start",
      "type": "n8n-nodes-base.start",
      "typeVersion": 1,
      "position": [
        150,
        850
      ]
    },
    {
      "parameters": {
        "functionCode": "Date.prototype.toIsoString = function () {\n  var tzo = -this.getTimezoneOffset(),\n    dif = tzo >= 0 ? '+' : '-',\n    pad = function (num) {\n      var norm = Math.floor(Math.abs(num));\n      return (norm < 10 ? '0' : '') + norm;\n    };\n  return this.getFullYear() +\n    '-' + pad(this.getMonth() + 1) +\n    '-' + pad(this.getDate()) +\n    'T' + pad(this.getHours()) +\n    ':' + pad(this.getMinutes()) +\n    ':' + pad(this.getSeconds()) +\n    dif + pad(tzo / 60) +\n    ':' + pad(tzo % 60);\n}\n\nfunction getMidnight(time) {\n  time.setHours(0, 0, 0, 0);\n  return time.toIsoString();\n}\n\nvar current_time_plain = new Date();\nvar current_time = current_time_plain.toIsoString();\n\nvar midnight_today = getMidnight(current_time_plain);\n\n// console.log('current: ' + current_time);\n// console.log('midnight: ' + midnight_today);\n\nitems[0].json.current_time = current_time;\nitems[0].json.midnight_today = midnight_today;\n\nreturn items;\n"
      },
      "name": "Today's Times",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        500,
        150
      ]
    },
    {
      "parameters": {
        "functionCode": "return items[0].json.map(item => {\n  return {\n    json: item\n  }\n});\n \n"
      },
      "name": "Split Time Entries",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        800,
        150
      ]
    },
    {
      "parameters": {
        "resource": "task",
        "additionalFields": {
          "client": "={{$node[\"Merge Time Entries\"].json[\"client_id\"]}}",
          "description": "={{$node[\"Merge Time Entries\"].json[\"description\"]}}",
          "project": "={{$node[\"Merge Time Entries\"].json[\"in_project_id\"]}}"
        },
        "timeLogsUi": {
          "timeLogsValues": [
            {
              "startDate": "={{$node[\"Merge Time Entries\"].json[\"start\"]}}",
              "endDate": "={{$node[\"Merge Time Entries\"].json[\"stop\"]}}"
            }
          ]
        }
      },
      "name": "Invoice Ninja",
      "type": "n8n-nodes-base.invoiceNinja",
      "typeVersion": 1,
      "position": [
        1450,
        550
      ],
      "credentials": {
        "invoiceNinjaApi": "Invoice Ninja API"
      }
    },
    {
      "parameters": {
        "url": "https://app.invoiceninja.com/api/v1/projects",
        "options": {},
        "headerParametersUi": {
          "parameter": [
            {
              "name": "X-Ninja-Token",
              "value": "ADD_YOUR_API_TOKEN"
            }
          ]
        }
      },
      "name": "Get IN Projects",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 1,
      "position": [
        500,
        950
      ]
    },
    {
      "parameters": {
        "url": "https://app.invoiceninja.com/api/v1/clients",
        "options": {},
        "headerParametersUi": {
          "parameter": [
            {
              "name": "X-Ninja-Token",
              "value": "ADD_YOUR_API_TOKEN"
            }
          ]
        }
      },
      "name": "Get IN Clients",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 1,
      "position": [
        500,
        750
      ]
    },
    {
      "parameters": {
        "functionCode": "let new_arr = [];\nlet new_obj = {};\n\nfor(let i in items[0].json.data){\n  value=items[0].json.data[i].id;\n  new_obj[value] = items[0].json.data[i]\n}\nfor(let i in new_obj){\n  new_arr.push(new_obj[i]);\n}\nreturn [{json:new_arr}];\n"
      },
      "name": "Get Clients Data",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        650,
        750
      ]
    },
    {
      "parameters": {
        "functionCode": "let new_arr = [];\nlet new_obj = {};\n\nfor(let i in items[0].json.data){\n  value=items[0].json.data[i].id;\n  new_obj[value] = items[0].json.data[i]\n}\nfor(let i in new_obj){\n  new_arr.push(new_obj[i]);\n}\nreturn [{json:new_arr}];\n"
      },
      "name": "Get Projects Data",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        650,
        950
      ]
    },
    {
      "parameters": {
        "functionCode": "return items[0].json.map(item => {\n  item = JSON.parse(JSON.stringify(item).split('\"id\":').join('\"in_project_id\":'));\n  return {\n    json: item\n  }\n});\n \n"
      },
      "name": "Split IN Projects",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        800,
        950
      ]
    },
    {
      "parameters": {
        "functionCode": "return items[0].json.map(item => {\n  delete item['name'];\n  item = JSON.parse(JSON.stringify(item).split('\"id\":').join('\"client_id\":'));\n  return {\n    json: item\n  }\n});\n \n"
      },
      "name": "Split IN Clients",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        800,
        750
      ]
    },
    {
      "parameters": {
        "mode": "mergeByKey",
        "propertyName1": "client_id",
        "propertyName2": "client_id",
        "overwrite": "blank"
      },
      "name": "Merge InvoiceNinja",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 1,
      "position": [
        1050,
        750
      ]
    },
    {
      "parameters": {
        "mode": "mergeByKey",
        "propertyName1": "pid",
        "propertyName2": "id"
      },
      "name": "Merge Toggl",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 1,
      "position": [
        1050,
        250
      ]
    },
    {
      "parameters": {
        "mode": "mergeByKey",
        "propertyName1": "name",
        "propertyName2": "name"
      },
      "name": "Merge Time Entries",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 1,
      "position": [
        1200,
        550
      ]
    },
    {
      "parameters": {
        "authentication": "basicAuth",
        "url": "https://api.track.toggl.com/api/v8/workspaces/ADD_YOUR_TOGGL_WORKSPACE_NUMBER/projects",
        "options": {},
        "queryParametersUi": {
          "parameter": []
        }
      },
      "name": "Get Toggl Projects",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 1,
      "position": [
        500,
        350
      ],
      "credentials": {
        "httpBasicAuth": "Toggl API Basic Auth"
      }
    },
    {
      "parameters": {
        "functionCode": "return items[0].json.map(item => {\n  return {\n    json: item\n  }\n});\n \n"
      },
      "name": "Split Toggl Projects",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        650,
        350
      ]
    },
    {
      "parameters": {
        "authentication": "basicAuth",
        "url": "https://api.track.toggl.com/api/v8/time_entries",
        "options": {},
        "queryParametersUi": {
          "parameter": [
            {
              "name": "start_date",
              "value": "={{$node[\"Today's Times\"].json[\"midnight_today\"]}}"
            },
            {
              "name": "end_date",
              "value": "={{$node[\"Today's Times\"].json[\"current_time\"]}}"
            }
          ]
        }
      },
      "name": "Get Time Entries",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 1,
      "position": [
        650,
        150
      ],
      "credentials": {
        "httpBasicAuth": "Toggl API Basic Auth"
      }
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "ADD_YOUR_N8N_WEBHOOK",
        "options": {}
      },
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 1,
      "position": [
        150,
        550
      ],
      "webhookId": "ADD_YOUR_N8N_WEBHOOK"
    },
    {
      "parameters": {
        "authentication": "basicAuth",
        "url": "https://api.track.toggl.com/api/v8/workspaces/ADD_YOUR_TOGGL_WORKSPACE_NUMBER/clients",
        "options": {},
        "queryParametersUi": {
          "parameter": []
        }
      },
      "name": "Get Toggl Clients",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 1,
      "position": [
        500,
        550
      ],
      "credentials": {
        "httpBasicAuth": "Toggl API Basic Auth"
      }
    },
    {
      "parameters": {
        "functionCode": "return items[0].json.map(item => {\n  item = JSON.parse(JSON.stringify(item).split('\"name\":').join('\"display_name\":'));\n  return {\n    json: item\n  }\n});\n"
      },
      "name": "Split Toggl Clients",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        650,
        550
      ]
    },
    {
      "parameters": {
        "mode": "mergeByKey",
        "propertyName1": "cid",
        "propertyName2": "id"
      },
      "name": "Merge Projects",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 1,
      "position": [
        900,
        350
      ]
    },
    {
      "parameters": {
        "mode": "mergeByKey",
        "propertyName1": "display_name",
        "propertyName2": "display_name"
      },
      "name": "Merge Clients",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 1,
      "position": [
        900,
        550
      ]
    }
  ],
  "connections": {
    "Today's Times": {
      "main": [
        [
          {
            "node": "Get Time Entries",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Time Entries": {
      "main": [
        [
          {
            "node": "Merge Toggl",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get IN Projects": {
      "main": [
        [
          {
            "node": "Get Projects Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get IN Clients": {
      "main": [
        [
          {
            "node": "Get Clients Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Projects Data": {
      "main": [
        [
          {
            "node": "Split IN Projects",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Clients Data": {
      "main": [
        [
          {
            "node": "Split IN Clients",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split IN Projects": {
      "main": [
        [
          {
            "node": "Merge InvoiceNinja",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Toggl": {
      "main": [
        [
          {
            "node": "Merge Time Entries",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge InvoiceNinja": {
      "main": [
        [
          {
            "node": "Merge Time Entries",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge Time Entries": {
      "main": [
        [
          {
            "node": "Invoice Ninja",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Toggl Projects": {
      "main": [
        [
          {
            "node": "Split Toggl Projects",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Toggl Projects": {
      "main": [
        [
          {
            "node": "Merge Projects",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Time Entries": {
      "main": [
        [
          {
            "node": "Split Time Entries",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Toggl Clients": {
      "main": [
        [
          {
            "node": "Split Toggl Clients",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Projects": {
      "main": [
        [
          {
            "node": "Merge Toggl",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Webhook": {
      "main": [
        [
          {
            "node": "Get Toggl Clients",
            "type": "main",
            "index": 0
          },
          {
            "node": "Get Toggl Projects",
            "type": "main",
            "index": 0
          },
          {
            "node": "Get IN Projects",
            "type": "main",
            "index": 0
          },
          {
            "node": "Get IN Clients",
            "type": "main",
            "index": 0
          },
          {
            "node": "Today's Times",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Toggl Clients": {
      "main": [
        [
          {
            "node": "Merge Clients",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Split IN Clients": {
      "main": [
        [
          {
            "node": "Merge Clients",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Clients": {
      "main": [
        [
          {
            "node": "Merge Projects",
            "type": "main",
            "index": 1
          },
          {
            "node": "Merge InvoiceNinja",
            "type": "main",
            "index": 1
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {}
}

So this is awesome. But I found that you may be able to simplify this by using the Toggl Node at the beginning, since it can already split out everything for you, and you can get the workspace id for it.

I don’t have a working example yet, but it seems like the better option

2 Likes

It wouldn’t surprise me to find out that there was a simpler way to do it. I’d love to see how it could be improved.

I’m pretty sure I started with both the Toggl node and the Invoice Ninja node. It’s been a minute, but my memory is that I needed to switch to the HTTP node to grab the Project information, because I don’t think either node provided that info out of the box. (See my conversation with @RicardoE105 above).

I wasn’t able to figure it out. TBH, If someone could step in and give this node some love, that would be greatly appreciated.

@artistro08 @matw you can make a feature request with the resources/operations that would simplify this workflow. Also, make sure that when you do so, you upvote the feature request.

1 Like