doGet is the function that Google Apps Script runs when someone opens the URL of your web app, or when a program sends it an HTTP GET request. Whatever doGet returns is what the visitor gets back: a web page, some plain text, or JSON data. That one function turns a script into a small website or a tiny API on top of your Google Sheet.
This post explains what doGet(e) is, what the event object e holds, and how to use it to return JSON from a sheet, greet a visitor safely, route by URL path, and how its sibling doPost receives data. It also covers the security choices that matter. If you are new to Apps Script, start with Google Apps Script for Beginners: A Simple Intro.
In this guide
The short version
doGet(e)runs when someone visits the web app URL or sends it a GET request.doPost(e)runs for POST requests.- It must return an HTML Service
HtmlOutputor a Content ServiceTextOutput. Anything else is an error. - Read the query string from
e.parameter(first value of each name) ore.parameters(arrays). - Nothing has a URL until you deploy the script as a web app, and code changes reach the public URL only in a new version.
- The two settings that decide security: Execute as and Who has access. Never print raw visitor input into HTML.
How the results in this post were produced. Apps Script runs only on Google’s servers, so the exact code shown was run against a small simulation of the Sheets service (an in-memory sheet with the same method names). The logic of the script is real, but the sheet and the log are simulated, and the clock was fixed at Monday 16 March 2026, 09:30 India time. Always try a script on a copy of your own sheet first.
For a web app, the requests were simulated too: the harness builds the event object e that Google would pass for the URL shown.
What doGet is
Google lists doGet(e) and doPost(e) among the special functions of Apps Script, together with onOpen and onEdit. doGet runs “when a user visits a web app or an HTTP GET request arrives”. A script becomes a web app when it has a doGet or doPost function that returns either:
- an HtmlOutput object (from
HtmlService), which the browser shows as a web page, or - a TextOutput object (from
ContentService), which is plain text, JSON, CSV or XML.
Both bound and standalone scripts can be web apps. Unlike onEdit, nothing is triggered by a spreadsheet. The trigger is an HTTP request.
The smallest doGet
This is a complete web app. The first function returns text and the second returns a tiny HTML page (you only need one doGet in a project, the second one has a different name so both can be shown):
function doGet() {
return ContentService.createTextOutput("Hello from doGet");
}
function doGetHtml() {
return HtmlService.createHtmlOutput("<h1>Hello from doGet</h1>");
}
Execution log (simulated)
Text output: Hello from doGet (type TEXT)
HTML output: <h1>Hello from doGet</h1>
The event object e
When a request arrives, Google passes an event object e to doGet. These are its main fields, from Google’s documentation:
| Field | What it holds |
|---|---|
| e.queryString | The query string of the URL, or null if there is none |
| e.parameter | An object of name to value. For a name that appears more than once, only the first value |
| e.parameters | The same, but every value is an array, so repeated names keep all their values |
| e.pathInfo | The URL path after /exec or /dev |
| e.postData | For doPost only: the request body (contents), its MIME type and length |
So a visit to .../exec?name=Asha&tag=a&tag=b gives:
function doGet(e) {
Logger.log("queryString: %s", e.queryString);
Logger.log("parameter: %s", JSON.stringify(e.parameter));
Logger.log("parameters: %s", JSON.stringify(e.parameters));
return ContentService.createTextOutput("ok");
}
Execution log (simulated)
queryString: name=Asha&tag=a&tag=b
parameter: {"name":"Asha","tag":"a"}
parameters: {"name":["Asha"],"tag":["a","b"]}
--- and a visit with no query string ---
queryString: null
parameter: {}
parameters: {}
The name tag was sent twice. e.parameter keeps only the first value, while e.parameters keeps both. With no query string, e.queryString is null and the objects are empty, so always give a default: e.parameter.name || "guest".
Use case 1: a JSON API from a sheet
Your sheet holds a product list, and you want another website or app to read it. doGet reads the rows, optionally filters them by ?category=, and returns JSON. Setting the MIME type to JSON tells the caller how to read it. The spreadsheet is opened by its id, which is the long part of the sheet’s URL, because a web app has no screen and no open file:
function doGet(e) {
const rows = SpreadsheetApp.openById("YOUR_SPREADSHEET_ID") // the id is in the sheet's URL
.getSheetByName("Products").getDataRange().getValues().slice(1);
const category = e.parameter.category; // undefined if the URL has no ?category=
const items = rows
.filter(row => !category || row[1] === category)
.map(row => ({ name: row[0], category: row[1], price: row[2] }));
return ContentService
.createTextOutput(JSON.stringify({ count: items.length, items: items }))
.setMimeType(ContentService.MimeType.JSON);
}
Responses (simulated)
no filter [JSON]
{"count":4,"items":[{"name":"Notebook","category":"Stationery","price":250},{"name":"Pen","category":"Stationery","price":40},{"name":"Bag","category":"Accessories","price":900},{"name":"Bottle","category":"Accessories","price":350}]}
?category=Stationery [JSON]
{"count":2,"items":[{"name":"Notebook","category":"Stationery","price":250},{"name":"Pen","category":"Stationery","price":40}]}
?category=Toys [JSON]
{"count":0,"items":[]}
Because the script runs as its owner, visitors can read this data without having access to the sheet itself. That is powerful, and it is the reason to think about what you return (see the security section below).
Use case 2: an HTML page, safely
Returning HTML is just as easy, but it has one serious trap: if you paste the visitor’s text into the page, they can inject their own script. Look at what happens when someone opens a link with ?name=<script>steal()</script>:
function doGetUnsafe(e) {
// BAD: the visitor's text goes straight into the HTML
return HtmlService.createHtmlOutput("<h1>Hello, " + e.parameter.name + "</h1>");
}
function doGetSafe(e) {
// GOOD: <?= ?> escapes the value before it is printed
const page = HtmlService.createTemplate("<h1>Hello, <?= name ?></h1>");
page.name = e.parameter.name || "guest";
return page.evaluate();
}
Pages produced (simulated)
Unsafe: <h1>Hello, <script>steal()</script></h1>
Safe: <h1>Hello, <script>steal()</script></h1>
Safe with no name: <h1>Hello, guest</h1>
The unsafe version prints the visitor’s script tag as it is, and a browser would run it. The template version, with <?= ?>, escapes the value, so the browser only shows it as text. Google’s documentation recommends the printing scriptlet <?= ?> as the default and warns to use the force-printing <?!= ?> only for content you fully control. Templates get their own treatment in Build a Web App Frontend With Apps Script.
Use case 3: routing by path
A web app has one URL, but e.pathInfo holds anything added after /exec, so one doGet can serve several “pages”:
function doGet(e) {
switch (e.pathInfo) { // the part of the URL after /exec
case "pricing":
return ContentService.createTextOutput("Pricing page");
case "about":
return ContentService.createTextOutput("About page");
default:
return ContentService.createTextOutput("Home page");
}
}
Responses (simulated)
/exec/ -> Home page
/exec/pricing -> Pricing page
/exec/about -> About page
/exec/contact -> Home page
doPost: receiving data
GET is for asking. When another system needs to send data, for example a form on another site, or a webhook, it makes a POST request, and doPost(e) runs. The body of the request is in e.postData.contents, as text, and its type in e.postData.type. This one adds a signup to a sheet and answers with JSON:
function doPost(e) {
const data = JSON.parse(e.postData.contents); // the body of the request, as text
const sheet = SpreadsheetApp.openById("YOUR_SPREADSHEET_ID").getSheetByName("Signups");
sheet.appendRow([data.name, data.email]);
return ContentService
.createTextOutput(JSON.stringify({ status: "ok", row: sheet.getLastRow() }))
.setMimeType(ContentService.MimeType.JSON);
}
Response and sheet (simulated)
Response: {"status":"ok","row":2}
| A | B | |
|---|---|---|
| 1 | Name | |
| 2 | Asha | asha@example.com |
| doGet(e) | doPost(e) | |
|---|---|---|
| Runs on | HTTP GET (visiting a URL) | HTTP POST (sending data) |
| Data arrives in | The URL: e.parameter, e.pathInfo | The body: e.postData.contents |
| Typical use | Show a page, return data | Receive a form or a webhook, write a row |
| Must return | HtmlOutput or TextOutput | HtmlOutput or TextOutput |
Getting a URL: deploy as a web app
Until you deploy it, your doGet has no address. In the editor:
- At the top right, click Deploy, then New deployment.
- Next to “Select type”, choose Web app.
- Fill in the description and the two access settings (below).
- Click Deploy, approve the permissions, and copy the web app URL. It ends in
/exec.
There is also a test deployment with a URL ending in /dev. Only people with edit access to the script can open it, and it always runs your latest saved code. The full picture, with versions and updates, is in Apps Script Deployment Types Explained.
Who can call it
Two settings control the safety of a web app. Execute as decides whose account runs the script: you (the owner, always) or the user who is using the web app. Who has access decides who may open the URL. In the manifest file (appsscript.json) the four access values are:
| Value | Meaning |
|---|---|
| MYSELF | Only the person who deployed it |
| DOMAIN | Only users in the same Google Workspace domain as the deployer |
| ANYONE | Any signed-in Google user |
| ANYONE_ANONYMOUS | Anyone, even if not signed in |
The deployment dialog offers the same choices in plain words. The combination to be careful with is Execute as me plus Anyone: then strangers run code with your permissions. That is fine for a small public read-only feed, but you must make sure that:
- the script returns only data you are happy to publish (never the whole sheet by accident);
- visitor input is treated as untrusted: escape it in HTML, and validate it before it goes into a sheet;
- no secrets, such as API keys, appear in what it returns;
- write actions (
doPost) check what they receive, since anyone with the URL can call them.
Testing and updating
You cannot test doGet(e) by pressing Run, because nothing passes an event, and e.parameter fails on undefined. Either open the /dev URL of a test deployment in your browser, or write a test function that calls doGet with a made-up event, as the examples on this page do. Then check Executions for errors.
Updating is the step people forget. The /exec URL serves a version of your code. After changing the code you have to create a new version and point the deployment at it (Deploy, then Manage deployments, then edit the deployment). The URL stays the same. Only the /dev URL always runs the latest saved code.
Common mistakes
- Returning the wrong thing.
doGetmust return anHtmlOutputorTextOutput. Returning a string or an object gives an error page. - Forgetting to redeploy. Saving the code does not change the
/execURL. Create a new version and update the deployment. - No default for missing parameters.
e.parameter.nameis undefined when the URL has no?name=. Use|| "default". - Printing visitor input as raw HTML. Use a template with
<?= ?>so that the value is escaped. - Relying on
getActiveSpreadsheet(). A web app has no open file, so open the sheet explicitly withSpreadsheetApp.openById(). - Wrong access setting. If visitors see a sign-in or permission page, “Who has access” is too narrow for them.
- Exposing too much. With “Execute as me” and “Anyone”, everything the script returns is public. Return only what you mean to share.
- Testing with the Run button. There is no event. Use the
/devURL or a test function.
Try it yourself
Work out each answer first, then open the solution.
1. Write a doGet that returns the current date and time as plain text.
Show solution
function doGet() {
const now = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), "yyyy-MM-dd HH:mm");
return ContentService.createTextOutput("The time is " + now);
}
Response (simulated)
The time is 2026-03-16 09:30ContentService returns a TextOutput, which is allowed. Utilities.formatDate() formats the time in the script’s time zone.
2. Greet the visitor with ?name=, and say “friend” if no name is given. Do it safely.
Show solution
function doGet(e) {
const name = e.parameter.name || "friend";
const page = HtmlService.createTemplate("<p>Welcome, <?= name ?>!</p>");
page.name = name;
return page.evaluate();
}
Pages produced (simulated)
<p>Welcome, Asha!</p>
<p>Welcome, friend!</p>The value goes into a template and is printed with <?= ?>, which escapes it.
3. Return the number of products in the sheet as JSON.
Show solution
function doGet() {
const count = SpreadsheetApp.openById("YOUR_SPREADSHEET_ID")
.getSheetByName("Products").getLastRow() - 1; // minus the header row
return ContentService.createTextOutput(JSON.stringify({ products: count }))
.setMimeType(ContentService.MimeType.JSON);
}
Response (simulated)
{"products":4}The last row minus the header is the number of products. Setting the MIME type to JSON tells the caller how to read the reply.
4. A URL contains ?id=7&id=9. What are e.parameter.id and e.parameters.id?
Show answer
e.parameter.id is the first value, "7". e.parameters.id is the array ["7", "9"]. All values arrive as text, so convert with Number() when you need a number.
Frequently asked questions
What is doGet in Google Apps Script?
It is a special function that runs when a user visits a web app URL or when an HTTP GET request is sent to it. It receives an event object e and must return an HtmlOutput or a TextOutput.
What is the difference between doGet and doPost?
doGet handles GET requests, where the data is in the URL. doPost handles POST requests, where the data is in the request body (e.postData). Both must return an HtmlOutput or TextOutput.
How do I get the URL parameters in doGet?
Use e.parameter for the first value of each name, or e.parameters for arrays of all values. For example, e.parameter.name for ?name=Asha.
How do I return JSON from Apps Script?
Return ContentService from doGet.
Why do I get an error when I run doGet in the editor?
The editor calls the function without a request, so e is undefined. Deploy the script and open the test URL, or call doGet from a test function with a made-up event.
Is a doGet web app public?
It depends on the deployment setting “Who has access”. It can be only you, your domain, any signed-in Google user, or anyone. Combined with “Execute as me”, think carefully about what your script returns.
Related reading
- Build a Web App Frontend With Apps Script – HTML pages that talk to your script.
- Apps Script Deployment Types Explained – versions, test URLs and access settings.
- Google Apps Script for Beginners: A Simple Intro – the basics.
- Apps Script Time Triggers: Cron for Google Sheets – run code on a schedule.