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
We can use “On form submission” as a Trigger
I’ll replicate the same fields (email, summary and description).
I can click “Test Step” to see how it will look
I can look at the Javascript used by my feedback form to remind myself which repo processes requests
Let’s start with the simplest solution which is to trigger a Github workflow event
My first attempts were to use the Github core action and “Dispatch” as well as “Dispatch and Wait for Completion” flows
But executing would give me errors about Invalid JSON because I needed to wrap the text with double quotes
As well as errors about the Workflow not having a “workflow_dispatch” trigger
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”)
I should be able to use the proper auth type here without having to re-use a short lived lower credentialed GHP token
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
(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
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();
Next, I think we can use a better name than “Workflow 3” for the workflow
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"
}
]
}
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
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)
I could add steps for a lot of my integrations including Datadog, SES, Discord, Telegram, Github and JIRA
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
I’ll now build out the form including a CC
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
However, I can use a generic HTTP POST for it
which worked
By re-arranging my flow, I can create the GH issue first then reference it in the Discord step
Which looks as such when posting
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)
However, using a “Merge” node can bring the fields together
I can do what I did for Discord to make Keybase work as well
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
As before, I used a merge step to combine the Token and the cleansed Form Elements.
This worked great to post to Vikunja
Let’s do a full run of the flow
I didn’t show it above, but I can also confirm it posted to Keybase
Now with the feedback form completed
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
This creates an Agentic AI flow that leverages OpenAI
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
I’ll now create a new OpenAI key for use with n8n
I can now save that in my n8n credential store
We see a basic confirmation that it works when saving
I’m going to leave the default model (gpt-4o-mini) but I could change it
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)”
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
I now have an “Organization” in OpenAI
As the key we made earlier persists
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
And the OAuth connection is successful
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
I now have the Agentic AI setup to use local memory in n8n and save events to the Google Calendar
Here you can see it in action
Once I set a Shared calendar that had events for Saturday, we could see some results
Let’s say we wanted to make our family calendar public, I could edit my flow and say “May Chat Publicly Available”
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:
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
What did that cost? It was about 3/10 of 1c (for input + output):
As I didn’t want to expose my family cal, I disabled the workflow
And now that Webhook chat page shows a 404 JSON message
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
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.
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.