Like many freelance consultants, I charge clients by the hour and bill every two weeks. The problem is sometimes I accumulate quite a bit of time between invoices and some clients are surprised when they see the invoice for the last two weeks.
I thought to myself: there’s gotta be a better way to let people know how much time you’re accumulating without having to constantly remind them, send them emails, or wait for them to reach out.
I ended up building custom AirTable views for my clients to see their own hours in real time as I tracked it using Toggl. To pull the systems together, I used n8n (an open-source alternative to Zapier).
Here’s the video I recorded on how to do this yourself:
Here is the JSON code to add this automation into your N8N instance (explained near the end of the video):
{
"nodes": [
{
"parameters": {
"triggerTimes": {
"item": [
{
"mode": "everyX",
"value": 12
}
]
}
},
"name": "Cron",
"type": "n8n-nodes-base.cron",
"typeVersion": 1,
"position": [
450,
450
]
},
{
"parameters": {
"functionCode": "var date = new Date();\nvar firstDay = new Date(date. getFullYear(), date. getMonth(), 1).toJSON().split(\"T\")[0];\nvar midmonth = new Date(date. getFullYear(), date. getMonth(), 16).toJSON().split(\"T\")[0];\nvar currentday = new Date().getDate();\n\nif (currentday > 16 || currentday == 1) {\nitems[0].json.togglday = midmonth;\n}\nelse {\nitems[0].json.togglday = firstDay;\n}\n\nitems[0].json.firstday = firstDay;\nitems[0].json.midmonth = midmonth;\nitems[0].json.currentday = currentday;\n\nreturn items;"
},
"name": "Get Dates",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
650,
450
]
},
{
"parameters": {
"authentication": "basicAuth",
"url": "=https://api.track.toggl.com/reports/api/v2/summary?workspace_id=2384903234823&user_agent=n8napi&grouping=clients&since={{$node[\"Get Dates\"].json[\"togglday\"]}}",
"options": {},
"headerParametersUi": {
"parameter": [
{
"name": "Content-type",
"value": "application/json"
}
]
}
},
"name": "Toggl Report",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 1,
"position": [
840,
450
],
"credentials": {
"httpBasicAuth": "Toggl"
}
},
{
"parameters": {
"operation": "update",
"application": "iofdjsf3208",
"table": "Table 1",
"id": "={{ $node[\"Get Time Spent and AirTable ID\"].json[\"airtableid\"] }}",
"updateAllFields": false,
"fields": [
"Hours"
],
"options": {}
},
"name": "Airtable",
"type": "n8n-nodes-base.airtable",
"typeVersion": 1,
"position": [
2140,
260
],
"credentials": {
"airtableApi": "Datos AirTable"
}
},
{
"parameters": {
"values": {
"number": [
{
"name": "Hours",
"value": "={{$node[\"Get Time Spent and AirTable ID\"].json[\"timespent\"]}}"
}
]
},
"options": {}
},
"name": "Set",
"type": "n8n-nodes-base.set",
"typeVersion": 1,
"position": [
2000,
460
]
},
{
"parameters": {
"operation": "list",
"application": "23ryu29hf9723f",
"table": "Table 1",
"additionalOptions": {
"filterByFormula": "={Name}=\"{{$node[\"SplitInBatches\"].json[\"title\"][\"client\"]}}\""
}
},
"name": "Get AirTable field by name",
"type": "n8n-nodes-base.airtable",
"typeVersion": 1,
"position": [
1390,
460
],
"credentials": {
"airtableApi": "Datos AirTable"
}
},
{
"parameters": {
"functionCode": "/*const results = []\n\nconst events = items[0].json[\"data\"]\n\nfor (event of events) {\n results.push({ json: event })\n}\n\nreturn results;*/\n\nreturn items[0].json.data.map(item => {\n return {\n json: item\n }\n});\n"
},
"name": "Function",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
1030,
450
]
},
{
"parameters": {
"functionCode": "var time = $item(\"0\").$node[\"SplitInBatches\"].json[\"time\"] / 3600000;\nvar airtableid = $item(\"0\").$node[\"Get AirTable field by name\"].json[\"id\"];\nvar clientemail = $item(\"0\").$node[\"Get AirTable field by name\"].json[\"fields\"][\"Email\"];\nvar duedate = new Date();\nduedate.setDate(duedate.getDate() + 15);\n\nreturn [{json: \n{\"timespent\": time, \"airtableid\": airtableid, \"clientemail\": clientemail, \"duedate\": duedate}\n}];\n"
},
"name": "Get Time Spent and AirTable ID",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
1770,
460
]
},
{
"parameters": {
"batchSize": 1,
"options": {
"reset": false
}
},
"name": "SplitInBatches",
"type": "n8n-nodes-base.splitInBatches",
"typeVersion": 1,
"position": [
1210,
450
]
}
],
"connections": {
"Cron": {
"main": [
[
{
"node": "Get Dates",
"type": "main",
"index": 0
}
]
]
},
"Get Dates": {
"main": [
[
{
"node": "Toggl Report",
"type": "main",
"index": 0
}
]
]
},
"Toggl Report": {
"main": [
[
{
"node": "Function",
"type": "main",
"index": 0
}
]
]
},
"Airtable": {
"main": [
[
{
"node": "SplitInBatches",
"type": "main",
"index": 0
}
]
]
},
"Set": {
"main": [
[
{
"node": "Airtable",
"type": "main",
"index": 0
}
]
]
},
"Get AirTable field by name": {
"main": [
[
{
"node": "Get Time Spent and AirTable ID",
"type": "main",
"index": 0
}
]
]
},
"Function": {
"main": [
[
{
"node": "SplitInBatches",
"type": "main",
"index": 0
}
]
]
},
"Get Time Spent and AirTable ID": {
"main": [
[
{
"node": "Set",
"type": "main",
"index": 0
}
]
]
},
"SplitInBatches": {
"main": [
[
{
"node": "Get AirTable field by name",
"type": "main",
"index": 0
}
]
]
}
}
}