Submitting a Google form to Jira. Really? Why? There are good reasons.
Hello, after a long break, I'm back.
Recently my wife came to me for a quick brainstorm: could we submit Jira tickets via a Google Form? Why, I asked her, exactly what you're thinking right now. Her answer: it's easy to build a form, it cuts down on tooling costs, and it lets developers focus on more important things than manually filing tickets for the same five requests every week, forever, until the heat death of the universe.
Challenge accepted.
Surely there's a Jira API for this, with solid documentation, right? There is — and finding the right page in it was surprisingly painful, in a way that made me question my life choices. I expected this to be a quick afternoon project. Instead I got a guided tour of Atlassian's documentation architecture, which I did not ask for and did not enjoy. Consider this post the shortcut I wish someone had handed me:
- Personal access tokens — how to get one.
- Jira REST API examples — the JSON shape for the POST request.
- The Jira Data Center REST API — the actual headers and endpoint details.
Three separate links because, evidently, Atlassian believes in cardio.
Obtaining the personal access token
Head to your Atlassian account settings, then Security → API tokens, and create a new one. Copy it somewhere safe — treat it like a house key, not a fun little string of characters, because it opens real doors.
Create the Request Headers and Post Request
As always, I'm attaching a real Postman collection and environment below so you don't have to reverse-engineer any of this from scratch like I did.
The request body is refreshingly simple — almost suspiciously simple, given how much documentation-spelunking it took to find it — just the project key, a summary, description, issue type, assignee, and priority:
{
"fields": {
"project": {
"key": "yourprojectkey"
},
"summary": "Summary of the task",
"description": "Hello World Task Created",
"issuetype": {
"name": "Task"
},
"assignee": {
"id": "{{asigneeGroupId}}"
},
"priority": {
"name": "High"
}
}
}The project key comes from your list of Jira projects. The assignee can be a group or a person — either way, Jira wants an ID, not a name, because names are for humans and Jira has never cared much for humans. You'll find that ID sitting right there in the URL when you're looking at the group or user in the admin console. Everything else in the body is exactly as self-explanatory as it looks, a rare and welcome mercy.
Now, the environment — six variables carry the whole operation:
apiToken— the personal access token you just created.apiUser— your email, not your username. Let me say that again, slower, because it personally cost me time: email, not username.prefixUrl— the subdomain of your Jira instance. If your Jira lives athttps://mycompany.atlassian.net/jira, the prefix ismycompany.jiraUrl— the actual API target, built from the prefix:https://{{prefixUrl}}.atlassian.net/rest/api/2/issue.assigneeGroupId— whoever, or whatever group, inherits this ticket.projectKey— the project key you tracked down a minute ago.
Set the request's authorization to Basic Auth using apiUser/apiToken, fire off the POST, and — success. A shiny new Jira ticket, summoned entirely from the command line, no browser, no clicking through six menus to find the “Create” button that Jira insists on hiding.
A few honest gripes while I'm here: Basic Auth, in this day and age? A bearer token with actual scopes would have been nice, and would have let me sleep slightly better. The documentation also doesn't fully enumerate every possible field and value — a list of valid enums would go a long way, Atlassian, I believe in you. And the API versioning scheme is baffling — /rest/api/2/ for something that is very clearly not a leisurely stroll through only two major versions of anything. But it works, and at the end of the day, that's the only version number that actually matters to anyone shipping software.
Google form
Time for the actual form. I started from Google's sample form template rather than building one from scratch — no sense reinventing a form about, of all things, forms. That would be a special kind of recursive suffering.
In the form's settings, enable “Collect email addresses” (verified) — this is what lets the script later identify who actually submitted the form, so you know who to email back once the ticket exists in the wild.
Now, the script. Open the Script Editor from the form, and drop in this Apps Script — note the function name, onFormSubmitDeux, which I can only assume means this was not, in fact, my first attempt, and there is an onFormSubmit somewhere in my past I'd rather not discuss:
/**
* Summary. Jira Issue Creation.
*
* Description. NodeJS script that handles the creation of issues in the JIRA API
* using Google Forms as the backplane.
*
* Author. Darth Seldon.
*/
function onFormSubmitDeux(e) {
//member variables
var formId = "formid";
var basicHeaderBase64 = "Basic (base64 some@email.com:token)";
var projectKey = "projectKey";
var description = "Task\n";
var issueType = "Task";
var summary = "Summary";
var priority = "High";
var assigneeGroupId = "assigneeGroupId";
var url = "https://mycompany.atlassian.net/rest/api/2/issue";
var noEmailTarget = "some@email.com";
var emailTitle = "Task Email";
//Form information
var form = FormApp.openById(formId);
var formResponses = form.getResponses();
var lastResponse = formResponses.length;
var formTitle = form.getTitle();
var formResponse = formResponses[lastResponse - 1];
var itemResponses = formResponse.getItemResponses();
var email = formResponse.getRespondentEmail();
for (var j = 0; j < itemResponses.length; j++) {
var itemResponse = itemResponses[j];
var title = itemResponse.getItem().getTitle();
var answer = itemResponse.getResponse();
description += title + ": " + answer + "\n";
}
//Jira Request
var headers = {
"content-type": "application/json",
"Accept": "application/json",
"authorization": basicHeaderBase64
};
//Request Data
var data = {
"fields": {
"project": {
"key": projectKey
},
"summary": summary,
"description": description,
"issuetype": {
"name": issueType
},
"assignee": {
"id": assigneeGroupId
},
"priority": {
"name": priority
}
}
}
var payload = JSON.stringify(data);
var options = {
"content-type": "application/json",
"method": "POST",
"headers": headers,
"payload": payload
};
var response = UrlFetchApp.fetch(url, options);
//Jira Response
if (!email.length) {
email = noEmailTarget;
}
if(!formTitle.length) {
formTitle = emailTitle;
}
var data = JSON.parse(response.getContentText());
var emailBody = "Task\n" + "https://mycompany.atlassian.net/jira/core/projects/" + projectKey + "/issues/" + data.key;
MailApp.sendEmail(email, formTitle, emailBody);
}Functionally, this is the same idea as the Postman request — it just also loops through every question/answer pair from the most recent form submission to build the description, and emails the respondent a direct link to their brand new ticket once it exists.
A few things you'll need to change before this does anything useful:
formId— the ID sitting in your form's URL, used to pull that specific form's responses.basicHeaderBase64— replace the placeholder with the actual Base64 encoding ofsome@email.com:token. If you don't have a tool handy for that, base64encode.org will do it in about four seconds, no installation required, no npm package with 400 transitive dependencies.assigneeGroupId,url, andnoEmailTarget— your real group/person ID, your actual Jira instance URL (swap outmycompany), and the fallback email address for tickets submitted by the anonymously brave.
Save the script once everything's filled in.
OnSubmit event triggering
Under Triggers, add a new one, point it at onFormSubmitDeux, and set the event type to “On form submit.”
This is the part that ate an entire day of my life, and not in the fun way. While testing, I kept hitting bizarre permission errors — UrlFetchApp and MailApp.sendEmail suddenly refusing to work, no clear reason given, just vibes and rejection. The fix, once I finally found it after enough frustration to power a small city: delete the trigger and recreate it.
The actual reason is subtler than it sounds: Google infers the script's required permission scopes at the exact moment you save it, and grants only those. Add a new external call or API dependency later, and the existing trigger just keeps running under its old, narrower scope — blissfully unaware it now needs more permissions to do its job. A fresh trigger re-evaluates the scopes from scratch and actually picks up the new requirements. The trigger, in other words, needed to be told there was a promotion available.
Submit the form, and you can watch the execution log confirm it ran (or tell you, in painful detail, exactly why it didn't).
I don't know how many people will actually find a genuine use for submitting Jira tickets via Google Forms, but I personally picked up a couple of very real Apps Script lessons chasing this one down, and at 2am that felt like a fair trade. Happy coding!!!