Send Attachments from Airtable to Clickup

Hello everyone,

My first post and I hope it makes sense. I’ve spent days going through other similar topics here and nothing seems to solve my scenario. This is what I’m doing:

  1. A webhook triggers my flow with Data from the AT record.
  2. I can successfully call the record (and its attachments, using the fancy “Download Attachments” functionality, thanks for that :slight_smile: )
  3. I Can create a task on clickup with the details from Airtable, except adding Attachments to it. I understand that attachments are handled a bit differently by the clickup API.
  4. I added a HTTP request to add the attachments, and it works as intended, but only if there’s ONE attachment. If there’s more than one, it only sends one, because I hardcoded the Binary Property (attachment:Adjuntos_0). So here are my questions:

a) How can format the binary results of the airtable list operation in a way that I could use it in the next HTTP request to send it to Clickup? I think it has something to do with a custom code that iterates through the binary part of the items and outputs what i’m hardcoding.

b) I noticed that the binaries I get in my first "airtable list " operation are gone after I execute the clickup “create task” operation (that’s why added a second one). Would be possible to keep that binary data? It is kept after the function “Choose auth + assignee” but not after clickup “create task” node.

c) Is in the short term roadmap to have a Clickup “Add attachments” operation/node? :sweat_smile:

Apologies for the long text, english is not my first language I want to make sure i’m clear enough to get some help. :nerd_face:

Thanks in advance,

Welcome to the community. @infinitegroup

It’s a bit tricky. Since the ClickUp API allows you to upload just one file at a time, you need to split the binary data into multiple records. Check the example below.

My Airtable table looks like this:

When I upload the attachments to ClickUp, it looks like this:

You can make a separate feature request for the attachments resource. When you create the post, make sure to add it to the category feature request.

{
  "nodes": [
    {
      "parameters": {},
      "name": "Start",
      "type": "n8n-nodes-base.start",
      "typeVersion": 1,
      "position": [
        250,
        300
      ]
    },
    {
      "parameters": {
        "operation": "list",
        "application": "appXK4YYl2hKGCNCJ",
        "table": "T1",
        "downloadAttachments": true,
        "downloadFieldNames": "Attachment",
        "additionalOptions": {}
      },
      "name": "Airtable",
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 1,
      "position": [
        520,
        300
      ],
      "credentials": {
        "airtableApi": "asasasa"
      }
    },
    {
      "parameters": {
        "functionCode": "const results = [];\n\nfor (const item of items) {\n  for (const key of Object.keys(item.binary)) {\n      results.push({ \n      json: { \n        binaryProperty: key,\n        fileName: item.binary[key].fileName\n      },\n      binary: { \n        [key]: \n        item.binary[key]\n      }\n    })\n  }\n}\n\nreturn results;"
      },
      "name": "Function",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        740,
        300
      ]
    },
    {
      "parameters": {
        "authentication": "headerAuth",
        "requestMethod": "POST",
        "url": "https://api.clickup.com/api/v2/task/429rc2/attachment",
        "jsonParameters": true,
        "options": {
          "bodyContentType": "multipart-form-data"
        },
        "sendBinaryData": true,
        "binaryPropertyName": "=attachment:{{$node[\"Function\"].json[\"binaryProperty\"]}}"
      },
      "name": "HTTP Request",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 1,
      "position": [
        1020,
        300
      ],
      "credentials": {
        "httpHeaderAuth": "Clickup"
      }
    }
  ],
  "connections": {
    "Start": {
      "main": [
        [
          {
            "node": "Airtable",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Airtable": {
      "main": [
        [
          {
            "node": "Function",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Function": {
      "main": [
        [
          {
            "node": "HTTP Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
1 Like

Thanks Ricardo, this definitely sets me in the right direction.

I just noticed that, when I hardcode the task URL, it works, but when I set it with an expression, it doesn’t.

For the error image above (POSTing to a url created with a expression), seems like the “task” value is getting lost and the POST fails.

The expression seems to be good (I copy and paste the results of the expression and hardcode it as URL, it works.)

Again, thanks for all the help and apologies but the solution doesn’t look obvious to me. :sweat_smile:

Regards,

Can you please share the expression you are using in the URL?

Sure thing!

Here it is:

And here are the nodes involved:

{
  "nodes": [
    {
      "parameters": {
        "team": "={{$node[\"Trigger Hook\"].json[\"query\"][\"TeamID\"]}}",
        "space": "={{$node[\"Trigger Hook\"].json[\"query\"][\"SpaceID\"]}}",
        "folderless": true,
        "list": "={{$node[\"Trigger Hook\"].json[\"query\"][\"ListID\"]}}",
        "name": "={{$node[\"Get Record Info\"].json[\"fields\"][\"Post\"]}}",
        "additionalFields": {
          "assignees": "={{ [$node[\"Choose Auth + Assignee\"].json[\"AssigneeID\"]] }}",
          "customFieldsJson": "=[{\"id\": \"533ffc74-288b-4157-b482-b5393e23da6e\", \"value\":\"https://www.airtable.com/{{$node[\"Trigger Hook\"].json[\"query\"][\"TableID\"]}}/{{$node[\"Trigger Hook\"].json[\"query\"][\"ViewID\"]}}/{{$node[\"Trigger Hook\"].json[\"query\"][\"recordID\"]}}\"}]",
          "content": "={{$node[\"Get Record Info\"].json[\"fields\"][\"Concepto del Diseño\"]}}",
          "dueDate": "={{$node[\"Choose Auth + Assignee\"].json[\"DueDateStamp\"]}}",
          "markdownContent": true,
          "priority": "={{$node[\"Choose Auth + Assignee\"].json[\"Priority\"]}}"
        }
      },
      "name": "ClickUp",
      "type": "n8n-nodes-base.clickUp",
      "typeVersion": 1,
      "position": [
        830,
        -40
      ],
      "credentials": {
        "clickUpApi": "Clickup Infinite"
      }
    },
    {
      "parameters": {
        "operation": "list",
        "application": "={{$node[\"Trigger Hook\"].json[\"query\"][\"AppID\"]}}",
        "table": "={{$node[\"Trigger Hook\"].json[\"query\"][\"Table\"]}}",
        "returnAll": false,
        "limit": 1,
        "downloadAttachments": true,
        "downloadFieldNames": "Adjuntos",
        "additionalOptions": {
          "fields": [],
          "filterByFormula": "=(RecordID = \"{{$node[\"Trigger Hook\"].json[\"query\"][\"recordID\"]}}\")"
        }
      },
      "name": "Get Attachments from AT",
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 1,
      "position": [
        1040,
        -40
      ],
      "credentials": {
        "airtableApi": "Airtable API"
      }
    },
    {
      "parameters": {
        "functionCode": "const results = [];\n\nfor (const item of items) {\n  for (const key of Object.keys(item.binary)) {\n      results.push({ \n      json: { \n        binaryProperty: key,\n        fileName: item.binary[key].fileName\n      },\n      binary: { \n        [key]: \n        item.binary[key]\n      }\n    })\n  }\n}\n\nreturn results;\n"
      },
      "name": "Split Attachments",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        1210,
        -190
      ]
    },
    {
      "parameters": {
        "authentication": "headerAuth",
        "requestMethod": "POST",
        "url": "=https://api.clickup.com/api/v2/task/{{$node[\"ClickUp\"].json[\"id\"]}}/attachment",
        "jsonParameters": true,
        "options": {
          "bodyContentType": "multipart-form-data"
        },
        "sendBinaryData": true,
        "binaryPropertyName": "=attachment:{{$node[\"Split Attachments\"].json[\"binaryProperty\"]}}"
      },
      "name": "Send Attachments to Clickup",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 1,
      "position": [
        1410,
        -190
      ],
      "credentials": {
        "httpHeaderAuth": "Clickup Header Auth"
      }
    }
  ],
  "connections": {
    "ClickUp": {
      "main": [
        [
          {
            "node": "Get Attachments from AT",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Attachments from AT": {
      "main": [
        [
          {
            "node": "Split Attachments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Attachments": {
      "main": [
        [
          {
            "node": "Send Attachments to Clickup",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

Just change {$node["ClickUp"].json["id"]}} to {{$item(0).$node["ClickUp"].json["id"]}}.

1 Like

The good news? Works flawlessly. Thanks a lot :raised_hands:

The not so good news? I am very confused now. :sweat_smile: Would you mind explaining a bit further the new requirement of specifying item(0) in front of all the expressions after your function?

Thanks again!

Glad that it worked. We have answered that a couple of times in the posts below. If after reading it’s still not clear, simply come back to me. I guess it makes sense to write a post about it.

1 Like

Thanks! This definitely helps. Appreciate it!

Sorry for open up this topic. I am also trying to attach something to an existing task. The problem I have is that the clickup api requires the attachment to be called “attachment” in the multiform data part.

Note: Make sure that your file is named attachment otherwise the API will not recognize the file.

I have not found an option to set the name of it.

Welcome to the community @Julian_David_Rath

Since the node does not support uploading files (You can make a feature request here in the community ), you need to use the HTTP request node. In the link below, you can see how the HTTP node was set up to achieve that.

Yep used that one, but in the multifrom-data upload. The name of the attachment needs to be attachment. n8n defaults to file. I created a PR for the documentation: Update README.md by julian-r · Pull Request #644 · n8n-io/n8n-docs · GitHub

I give n8n permission to license my contributions on any terms they like. I am giving them this license in order to make it possible for them to accept my contributions into their project.

Ehm, why this? Thought you are on apache?
This sounds a bit unrespectful for contributors…

No, we are Apache 2 with Commons Clause licensed and so not OSI approved open-source:

You can find our license here: n8n/LICENSE.md at master · n8n-io/n8n · GitHub
And information about fair-code here: https://faircode.io/

2 Likes