n8n: form flows and agentic ai

Published: May 15, 2025 by Isaac Johnson

Earlier this week we touched on n8n covering how to set it up and do a couple of simple flows.

Today I want to expand on that by first covering form processing two ways: fetch and redirect as well as full reimplementation. In the former, we’ll handle parsing, cleansing and forwarding to Github and in the second, we’ll look at building a full issue tracking and notification system that includes Github Issues, Vikunja tasks, Discord notifications, Email (via SES), and Keybase.

In the second half of this article, we dig into n8n and agentic AI. We will build a fully functional AI agent using OpenAI as well as n8n to create a chatbot integrated with our Google Calendar reading out shared dates. I’ll cover the whole flow including OpenAI and GCP Google Calendar API setup. I’ll wrap with a demo and some notes on costs.

Form Processing

One of my issues with my current setup is that my feedback form at the top sometimes expires as it packs a low privileged Github PAT in there to trigger a workflow that then does a lot of things including creating tickets, updating chats and emailing me.

I’ve written before about having to fix but I thought, “Could n8n solve this?”

The form basically is just a Summary, Body and email address which is optional

/content/images/2025/05/n8n2-26.png

We can use “On form submission” as a Trigger

/content/images/2025/05/n8n2-27.png

I’ll replicate the same fields (email, summary and description).

/content/images/2025/05/n8n2-28.png

I can click “Test Step” to see how it will look

/content/images/2025/05/n8n2-29.png

I can look at the Javascript used by my feedback form to remind myself which repo processes requests

/content/images/2025/05/n8n2-30.png

Let’s start with the simplest solution which is to trigger a Github workflow event

/content/images/2025/05/n8n2-31.png

My first attempts were to use the Github core action and “Dispatch” as well as “Dispatch and Wait for Completion” flows

/content/images/2025/05/n8n2-32.png

But executing would give me errors about Invalid JSON because I needed to wrap the text with double quotes

/content/images/2025/05/n8n2-33.png

As well as errors about the Workflow not having a “workflow_dispatch” trigger

/content/images/2025/05/n8n2-34.png

I decided to pivot to using the standard HTTP Request with the proper Github Headers (X-Github-Api-Version) and JSON body which includes “on-demand-payload”)

/content/images/2025/05/n8n2-35.png

I should be able to use the proper auth type here without having to re-use a short lived lower credentialed GHP token

/content/images/2025/05/n8n2-36.png

It took me a few to debug the issue. I felt a bit sheepish that it was just placement of the $

With the fields correct, we can see the right sample values

/content/images/2025/05/n8n2-38.png

(after I fixed that doubled double quote):

{"event_type":"on-demand-payload",
"client_payload":{"userid":"
{{ $json.Email }}","summary":"
{{ $json.Summary }}","description":
"{{ $json.Details }}"}}

Here we can see it finally working through the flow:

However, if someone puts in double quotes or symbols, it can mess up the form delivery

/content/images/2025/05/n8n2-40.png

With a bit of tweaking, I think I have a sanitized flow that cleans up double quotes and markdown:

I created a Code node with the following javascript (based on this LLM example)

// Get the raw output from the previous node
let summaryCleanse = $input.first().json.Summary;

// Convert double quotes to single quotes and escape any remaining double quotes
$input.first().json.Summary = summaryCleanse.replace(/^```(?:\w+)?\s*[\r\n]+|[\r\n]+\s*```$/g, '')  // Remove code blocks
    .replace(/"/g, '\\"')  // Escape double quotes
    .trim();  // Trim whitespace

let detailsCleanse = $input.first().json.Details;

$input.first().json.Details = detailsCleanse.replace(/^```(?:\w+)?\s*[\r\n]+|[\r\n]+\s*```$/g, '')  // Remove code blocks
    .replace(/"/g, '\\"')  // Escape double quotes
    .trim();  // Trim whitespace

return $input.all();

/content/images/2025/05/n8n2-41.png

Next, I think we can use a better name than “Workflow 3” for the workflow

/content/images/2025/05/n8n2-42.png

In a bit of an annoying paywall decision, they blocked “Share” behind an “Upgrade your plan” option.

However, I can download the JSON and share it below (you can put into a notepad, save as FeedbackForm.json then use “Upload workflow” in the menu to copy my workflow)

{
  "name": "FeedbackForm",
  "nodes": [
    {
      "parameters": {
        "formTitle": "FeedbackForm",
        "formDescription": "I'll get back to you soon (if you provided contact)",
        "formFields": {
          "values": [
            {
              "fieldLabel": "Email",
              "fieldType": "email",
              "placeholder": "anonymous@dontemailme.com",
              "requiredField": true
            },
            {
              "fieldLabel": "Summary",
              "requiredField": true
            },
            {
              "fieldLabel": "Details",
              "fieldType": "textarea"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.formTrigger",
      "typeVersion": 2.2,
      "position": [
        220,
        0
      ],
      "id": "b20ff695-2df1-49b6-ae7c-dd28227f4f36",
      "name": "On form submission",
      "webhookId": "286ee153-11bd-44ca-ab47-b177f087e296"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.github.com/repos/idjohnson/workflowTriggerTest/dispatches",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "githubApi",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "X-GitHub-Api-Version",
              "value": "2022-11-28"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\"event_type\":\"on-demand-payload\",\"client_payload\":{\"userid\":\"{{ $json.Email }}\",\"summary\":\"{{ $json.Summary }}\",\"description\":\"{{ $json.Details }}\"}}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        660,
        0
      ],
      "id": "a6781764-1e7f-409c-b2c1-0fec4ce70310",
      "name": "HTTP Request",
      "credentials": {
        "githubApi": {
          "id": "d233m3q1YXxrpudd",
          "name": "GitHub account"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Get the raw output from the previous node\nlet summaryCleanse = $input.first().json.Summary;\n\n// Convert double quotes to single quotes and escape any remaining double quotes\n$input.first().json.Summary = summaryCleanse.replace(/^```(?:\\w+)?\\s*[\\r\\n]+|[\\r\\n]+\\s*```$/g, '')  // Remove code blocks\n    .replace(/\"/g, '\\\\\"')  // Escape double quotes\n    .trim();  // Trim whitespace\n\nlet detailsCleanse = $input.first().json.Details;\n\n$input.first().json.Details = detailsCleanse.replace(/^```(?:\\w+)?\\s*[\\r\\n]+|[\\r\\n]+\\s*```$/g, '')  // Remove code blocks\n    .replace(/\"/g, '\\\\\"')  // Escape double quotes\n    .trim();  // Trim whitespace\n\nreturn $input.all();"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        0
      ],
      "id": "c742271b-a556-4508-b496-c64798a4014f",
      "name": "Code"
    }
  ],
  "pinData": {},
  "connections": {
    "On form submission": {
      "main": [
        [
          {
            "node": "Code",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Request": {
      "main": [
        []
      ]
    },
    "Code": {
      "main": [
        [
          {
            "node": "HTTP Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": true,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "8d408b94-78c6-4345-b8c2-a0cf349bfc19",
  "meta": {
    "templateCredsSetupCompleted": true,
    "instanceId": "30decc2c2e2f0d1ef5cae3101d975562c56caa5eaccf613e399c5ae0a4395615"
  },
  "id": "xYJJoRUU2NTUNBKa",
  "tags": [
    {
      "createdAt": "2025-05-10T15:31:27.783Z",
      "updatedAt": "2025-05-10T15:31:27.783Z",
      "id": "mFf3OvuXz4brnzHA",
      "name": "Freshbrewed.science"
    }
  ]
}

/content/images/2025/05/n8n2-43.png

N8n For a Full Workflow

What I did above was just wrapping a Github workflow dispatch with an N8N form.

What about doing ALL of it in n8n. I mean, I have a lot of steps in there such as posting to Atlassian JIRA (which is now disabled)

  # Create in Hosted Atlassian
          cat >jiraJSON <<EOT
          {
            "fields": {
              "description": {
                "content": [
                  {
                    "content": [
                      {
                        "text": "$ :: Requested by $",
                        "type": "text"
                      }
                    ],
                    "type": "paragraph"
                  }
                ],
                "type": "doc",
                "version": 1
              },
              "issuetype": {
                "id": "10001"
              },
              "labels": [
                "UserAsk"
              ],
              "project": {
                "id": "10000"
              },
              "summary": "$"
            },
            "update": {}
          }
          EOT

          curl --request POST --url "https://freshbrewed.atlassian.net/rest/api/3/issue" -u 'isaac@freshbrewed.science':"`cat jiraapitoken | tr -d '\n'`" --header 'Accept: application/json' --header 'Content-Type: application/json' -d @jiraJSON 

But also sends email with SES

/content/images/2025/05/n8n2-44.png

among many other things.

I tend to use a secret store to hold my passwords, but Variables is yet-another feature locked out behind a paywall (feeling a bit like Portainer right now)

/content/images/2025/05/n8n2-45.png

I could add steps for a lot of my integrations including Datadog, SES, Discord, Telegram, Github and JIRA

/content/images/2025/05/n8n2-46.png

However, I would need to roll-my-own on Keybase, Vikunja and Plane.so.

For the sake of simplicity, let’s assume we want to build a form just for email, discord and github issues - this covers 90% of most teams neads.

My first issue was that the “AWS SES” action isn’t using the narrowly defined SES SMTP credentials my other flows do. This must be because it is authing with the AWS regular keys.

I used an existing SES user but added a new credential. Because it was old, I also had to update the permissions policy associated (added SendEmail as SendRawEmail was the old action):

{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Effect": "Allow",
			"Action": "ses:SendRawEmail",
			"Resource": "*"
		},
		{
			"Effect": "Allow",
			"Action": "ses:SendEmail",
			"Resource": "*"
		}
	]
}

Then my test worked

/content/images/2025/05/n8n2-48.png

I’ll now build out the form including a CC

/content/images/2025/05/n8n2-49.png

I tried moving on to Discord. Only in n8n we cannot use webhook auth and instead they want a Bot token.

I created a Bot but couldn’t really figure out how to add it to my channel. The UI just has a server pick list that is empty

/content/images/2025/05/n8n2-50.png

However, I can use a generic HTTP POST for it

/content/images/2025/05/n8n2-51.png

which worked

/content/images/2025/05/n8n2-52.png

By re-arranging my flow, I can create the GH issue first then reference it in the Discord step

/content/images/2025/05/n8n2-53.png

Which looks as such when posting

/content/images/2025/05/n8n2-54.png

I thought I might be able to direct two inputs to a step, but I realized it will just work with one at a time (i can pick Github here or the email forms, but not both)

/content/images/2025/05/n8n2-55.png

However, using a “Merge” node can bring the fields together

/content/images/2025/05/n8n2-56.png

I can do what I did for Discord to make Keybase work as well

/content/images/2025/05/n8n2-57.png

My last item to tackle is Vikunja as that is my primary ticket tool.

The flow looks like this presently (in Github)


    # Post to Vikunja
    echo '{"long_token":true,"password":"' > login.json
    az keyvault secret show --vault-name wldemokv --name vikunja-fbformuser-password -o json | jq -r .value | tr -d '\n' >> login.json
    echo '","username":"fbformuser"}' >> login.json

    curl -X POST -H "Content-Type: application/json" -d @login.json https://vikunja.steeped.space/api/v1/login > fbfu.token

    # Github uses "body" and Vikunja uses "description"
    sed -i 's/"body"/"description"/g' ./emailJson2.json

    # Projects 1 is Inbox
    curl -X PUT -H "Content-Type: application/json" -H "Accept: application/json" -H "Authorization: Bearer `cat fbfu.token | jq -r .token | tr -d '\n'`" https://vikunja.steeped.space/api/v1/projects/1/tasks -d @emailJson2.json

I’m going to need to automate that Bearer Token flow.

First, build a JSON payload with a fixed user and password to return a Bearer token

/content/images/2025/05/n8n2-58.png

As before, I used a merge step to combine the Token and the cleansed Form Elements.

/content/images/2025/05/n8n2-60.png

This worked great to post to Vikunja

/content/images/2025/05/n8n2-59.png

Let’s do a full run of the flow

I didn’t show it above, but I can also confirm it posted to Keybase

/content/images/2025/05/n8n2-61.png

Now with the feedback form completed

/content/images/2025/05/n8n2-62.png

I could expose the public URL: https://n8n.tpk.pw/form/286ee153-11bd-44ca-ab47-b177f087e296.

I changed the top navigation to link feedback to the new form and contact to the old one.

AI Flows

I’m wary of AI tools but I thought it was work seeing what this “AI Agent Workflow” might be about

/content/images/2025/05/n8n2-01.png

This creates an Agentic AI flow that leverages OpenAI

/content/images/2025/05/n8n2-02.png

I don’t really use OpenAI (just free tier), so let’s see if we can get this working without paying extra.

I’ll go to OpenAI Keys to create a key

/content/images/2025/05/n8n2-03.png

I’ll now create a new OpenAI key for use with n8n

/content/images/2025/05/n8n2-04.png

I can now save that in my n8n credential store

/content/images/2025/05/n8n2-05.png

We see a basic confirmation that it works when saving

/content/images/2025/05/n8n2-06.png

I’m going to leave the default model (gpt-4o-mini) but I could change it

/content/images/2025/05/n8n2-07.png

The configuration for the agent lets us set some rules/context in the “System Message” area and then some predefined prompts in “Prompt (User Message)”

/content/images/2025/05/n8n2-08.png

I can click “Test Step” to try engaging with the Agentic AI.

Sadly, it looks like I need to feed OpenAI some money to use this.

I decided to create an org and drop a fiver in there to see what that buys us

/content/images/2025/05/n8n2-10.png

I now have an “Organization” in OpenAI

/content/images/2025/05/n8n2-11.png

As the key we made earlier persists

/content/images/2025/05/n8n2-12.png

We should be able to just try the n8n flow again. And while it worked, I ran into an issue with the fact my n8n did not know its real URL and got hung up on “localhost”.

To fix, I removed the container and recreated with the right “N8N” environment variables

builder@builder-T100:~$ docker stop n8n
n8n
builder@builder-T100:~$ docker rm n8n
n8n
builder@builder-T100:~$ docker run -d --name n8n -p 5678:443 -e N8N_SECURE_COOKIE=false -e N8N_HOST=n8n.tpk.pw -e N8N_PROTOCOL=https -e N8N_PORT=443 -v /home/builder/n8n:/home/node/.n8n docker.n8n.io/n8nio/n8n
d01742c69b86685281ebd0091a7e832506c23b46418a698c718ed2064ece0376

You’ll notice a couple things above - I had to tell it that it is on port 443 or it would pack the port into the REST url for Oauth2 redirects (and it is not editable when we do AgentAI AI auth).

So I redirected the dedicated port “5678” to “443” on the container to make it still work.

I can now see the right URL

/content/images/2025/05/n8n2-13.png

And the OAuth connection is successful

/content/images/2025/05/n8n2-14.png

I can now configure the Google Calendar API with the Agentic AI. Here I picked a shared calendar I used with the meet app we built a month back

/content/images/2025/05/n8n2-15.png

I now have the Agentic AI setup to use local memory in n8n and save events to the Google Calendar

/content/images/2025/05/n8n2-16.png

Here you can see it in action

Once I set a Shared calendar that had events for Saturday, we could see some results

/content/images/2025/05/n8n2-18.png

Let’s say we wanted to make our family calendar public, I could edit my flow and say “May Chat Publicly Available”

/content/images/2025/05/n8n2-19.png

As you can see, we now have an AI chat bot that can pull from a shared Google calendar

I can now go to the GCP Console and see the API usage for the Calendar API:

/content/images/2025/05/n8n2-21.png

It looks like those successful chats and the 5 minutes of chatting I did record (but did not post here since I got jammed up on the localhost REST API problem) used about 18 requests and 12k tokens

/content/images/2025/05/n8n2-22.png

What did that cost? It was about 3/10 of 1c (for input + output):

/content/images/2025/05/n8n2-23.png

As I didn’t want to expose my family cal, I disabled the workflow

/content/images/2025/05/n8n2-24.png

And now that Webhook chat page shows a 404 JSON message

/content/images/2025/05/n8n2-25.png

Summary

Today we had a lot of fun with n8n. We built out a simple form driven Github workflow trigger using a Form trigger to a JavaScript node to an HTTP node

/content/images/2025/05/n8n2-42.png

We then moved on to build a complete replication of that Github workflow into n8n to cover the whole process from collection to processing, processing to ticket creation, and lastly on to notifications. This made use of the Github node to create an issue and an Oauth Bearer token two-step flow for Vikunja tasks. We used the “merge” node to combine outputs for use with Keybase, Discord and Email (by way of SES) notifications.

/content/images/2025/05/n8n2-62.png

Lastly, we tackled Agentic AI with an OpenAI based agent. We added in a simple Database (local) and Google Calender integration using the GCP Calendar API. In both GCP spend and OpenAI spend, this cost less than 1c.

/content/images/2025/05/n8n2-16.png

OpenSource Workflow n8n form ai openai

Have something to add? Feedback? You can use the feedback form

Isaac Johnson

Isaac Johnson

Cloud Solutions Architect

Isaac is a CSA and DevOps engineer who focuses on cloud migrations and devops processes. He also is a dad to three wonderful daughters (hence the references to Princess King sprinkled throughout the blog).

Theme built by C.S. Rhymes