Can you build a real app in Google AI Studio without writing the code yourself? Yes. Open Build mode, describe the app in plain English, and Gemini writes the project, shows a live preview, and for Android gives you a Kotlin app you can install on your own phone. In this guide we build one complete system, a credit ledger app for a shop called ShopLedger, with Google Sheets as its database. You get the full Apps Script code and the full prompt, ready to copy.
| At a glance | Details |
|---|---|
| What you build | ShopLedger: customers, sales and payments on Android, synced to Google Sheets |
| Tools | Google AI Studio (Build mode), Google Sheets, Google Apps Script |
| Coding needed | No. You copy the code and the prompt from this page |
| Cost | Free to build and test. 25 US dollars one time only if you publish on Google Play |
| Time | About an afternoon for the first working version |
Every step below has a checklist. Tick the items as you go; your ticks are saved in this browser, so you can close the page and come back.
I build business systems on Google Sheets and PHP for a living, so I read the tool the way a client project would test it: what works out of the box, where it stops, and what you have to add before real users touch it.

What Google AI Studio Build mode actually is
Google AI Studio is Google's browser workspace for Gemini. Its Build mode is an app builder: you chat with an agent, it creates and edits every file in the project, and a preview on the right updates as the code changes. You need only a Google account and a browser. Nothing is installed on your computer.
Build mode makes two kinds of apps:
- Web apps, the default. They can call Gemini, use Firebase for login and data, connect to Google Workspace APIs such as Sheets, Gmail, Drive and Calendar, and deploy to Google Cloud Run.
- Android apps, added at Google I/O 2026. The agent writes a native Kotlin project with Jetpack Compose and runs it in an Android emulator streamed to your browser.
There is one catch for Sheets users. Google's documentation says AI Studio Android apps are client-side only: no server code and no Workspace APIs. So an Android app cannot open your Google Sheet by itself. The fix is a free Google Apps Script web app that sits between the phone and the sheet, and that is exactly what we build in Step 2.
Building is free within a usage quota. On the free tier, what you type may be used to improve Google's products, so never paste real customer data into a prompt. Use demo names such as "Client 1" while you build.
Before you start: plan the app on paper
The biggest difference between a good result and a messy one is the prompt, and a good prompt comes from ten minutes of planning. Write down three things:
- The screens. Login, dashboard, a list, an add form, a detail view, settings. Name each one.
- The data. For each record, the fields and their types: name (text), phone (text), amount (number), date (date), type (sale or payment).
- The rules. What must never happen. An amount cannot be zero, a phone number must be unique, a customer with history cannot be deleted.
If you cannot write these down, the AI cannot guess them either. It will invent fields, and you will spend an hour undoing them. The prompt in Step 3 is this plan, written out in full.
Step 1: Open Build mode and pick a platform
- [ ] Go to aistudio.google.com and sign in with your Google account.
- [ ] Click Build in the left-hand menu.
- [ ] In the platform picker, select Android. If you skip this, you get a web app.
Do not type the prompt yet. The prompt needs your web app URL, so the backend comes first.
Step 2: Set up the Google Sheet and the Apps Script API

- [ ] Create a new Google Sheet and name it ShopLedger Data.
- [ ] Open Extensions > Apps Script, delete the sample code in
Code.gs, and paste the full code below. - [ ] Change
API_TOKENto your own long random secret, for example 30 mixed letters and numbers. Keep a copy; the prompt needs it. - [ ] Pick setup in the function list at the top and click Run. Allow the permissions. It creates the
CustomersandTransactionstabs with their headers. - [ ] Optional: pick setupDemoData and click Run. It adds 5 demo customers and 15 sales and payments, so the app has data to show on its first sync. Running it again does not create copies.
- [ ] Click Deploy > New deployment, choose type Web app, set Execute as to Me and Who has access to Anyone, then click Deploy.
- [ ] Copy the Web app URL that ends in
/exec. Open it in your browser: you should see"ShopLedger API is running".
What setup creates in your sheet
| Tab | Columns | Notes |
|---|---|---|
| Customers | id, name, phone, opening_balance, notes, updated_at, deleted | Phone is stored as text, so the leading zero stays |
| Transactions | id, customer_id, date, type, amount, note, updated_at, deleted | type is sale or payment; amounts are numbers you can total |
Full Code.gs
/**
* Developed by Mohammad Rameez Imdad (Rameez Scripts)
* WhatsApp: https://wa.me/923435347417 (For Custom Projects)
* YouTube: https://www.youtube.com/@rameezimdad (Subscribe for more!)
*
* ShopLedger API - Google Sheet backend for the Android app
*/
const API_TOKEN = 'change-me-to-a-long-random-secret';
const TABLES = {
Customers: ['id', 'name', 'phone', 'opening_balance', 'notes', 'updated_at', 'deleted'],
Transactions: ['id', 'customer_id', 'date', 'type', 'amount', 'note', 'updated_at', 'deleted']
};
const isMoney = c => c === 'amount' || c === 'opening_balance';
// run once from the editor: creates both tabs
function setup() {
const ss = SpreadsheetApp.getActive();
Object.entries(TABLES).forEach(([name, cols]) => {
const sh = ss.getSheetByName(name) || ss.insertSheet(name);
sh.getRange(1, 1, 1, cols.length).setValues([cols]).setFontWeight('bold');
sh.setFrozenRows(1);
// text cols keep phone zeros + ISO dates as typed
cols.forEach((c, i) => sh.getRange(2, i + 1, sh.getMaxRows() - 1, 1)
.setNumberFormat(isMoney(c) ? '#,##0.00' : '@'));
});
}
function doGet() {
return out_({ success: true, message: 'ShopLedger API is running' });
}
function doPost(e) {
let req;
try { req = JSON.parse(e.postData.contents); }
catch (err) { return out_({ success: false, message: 'Bad JSON' }); }
if (req.token !== API_TOKEN) return out_({ success: false, message: 'Unauthorized' });
try {
if (req.action === 'ping') return out_({ success: true, message: 'pong', time: now_() });
if (req.action === 'push') return out_(push_(req.table, req.rows || []));
if (req.action === 'pull') return out_(pull_(req.table, String(req.since || '')));
return out_({ success: false, message: 'Unknown action' });
} catch (err) {
return out_({ success: false, message: String(err.message || err) });
}
}
// upsert by id, newer updated_at wins
function push_(table, rows) {
const cols = TABLES[table];
if (!cols) return { success: false, message: 'Unknown table' };
if (rows.length > 500) return { success: false, message: 'Max 500 rows per call' };
const lock = LockService.getScriptLock();
lock.waitLock(20000); // one write at a time
try {
const sh = SpreadsheetApp.getActive().getSheetByName(table);
const vals = sh.getDataRange().getValues().slice(1);
const idx = new Map(vals.map((r, i) => [String(r[0]), i]));
const u = cols.indexOf('updated_at');
let added = 0, updated = 0, skipped = 0;
rows.forEach(o => {
if (!o.id) return skipped++;
const line = cols.map(c => c === 'deleted' ? (o.deleted ? 'yes' : '')
: isMoney(c) ? Number(o[c]) || 0 : String(o[c] == null ? '' : o[c]));
const at = idx.get(String(o.id));
if (at === undefined) { idx.set(String(o.id), vals.length); vals.push(line); added++; }
else if (String(line[u]) >= String(vals[at][u])) { vals[at] = line; updated++; }
else skipped++; // sheet copy is newer
});
if (vals.length) sh.getRange(2, 1, vals.length, cols.length).setValues(vals);
return { success: true, added, updated, skipped };
} finally {
lock.releaseLock();
}
}
// rows changed after "since" (empty = all)
function pull_(table, since) {
const cols = TABLES[table];
if (!cols) return { success: false, message: 'Unknown table' };
const time = now_();
const u = cols.indexOf('updated_at');
const rows = SpreadsheetApp.getActive().getSheetByName(table).getDataRange().getValues().slice(1)
.filter(r => r[0] !== '' && String(r[u]) > since)
.map(r => Object.fromEntries(cols.map((c, i) => [c, c === 'deleted' ? r[i] === 'yes' : r[i]])));
return { success: true, rows, time };
}
// hand edits in the sheet get a fresh updated_at so phones pull them
function onEdit(e) {
const sh = e.range.getSheet(), cols = TABLES[sh.getName()];
if (!cols || e.range.getRow() < 2) return;
const u = cols.indexOf('updated_at') + 1;
if (e.range.getColumn() === u && e.range.getNumColumns() === 1) return;
sh.getRange(e.range.getRow(), u, e.range.getNumRows(), 1).setValue(now_());
}
const now_ = () => new Date().toISOString().replace(/\.\d+Z$/, 'Z');
const out_ = o => ContentService.createTextOutput(JSON.stringify(o))
.setMimeType(ContentService.MimeType.JSON);
// run from the editor: 5 demo customers + 15 entries, safe to run again (fixed ids)
function setupDemoData() {
setup();
const at = now_(), day = n => new Date(Date.now() - n * 864e5).toISOString().slice(0, 10);
const customers = [1, 2, 3, 4, 5].map(n => ({
id: 'DEMO-C' + n, name: 'Client ' + n, phone: '0300' + (1000000 + n),
opening_balance: n * 100, notes: 'House ' + n + ', Street ' + n + ', Demo City',
updated_at: at, deleted: false
}));
const tx = [];
customers.forEach((c, i) => [0, 1, 2].forEach(k => tx.push({
id: 'DEMO-T' + (i * 3 + k + 1), customer_id: c.id, date: day(i + k * 3),
type: k === 1 ? 'payment' : 'sale', amount: k === 1 ? 500 : (k + 1) * 1000,
note: 'Demo entry ' + (i * 3 + k + 1), updated_at: at, deleted: false
})));
Logger.log(JSON.stringify([push_('Customers', customers), push_('Transactions', tx)]));
}
What each part of the code does
| Part | The app sends | What happens |
|---|---|---|
| ping | {"token", "action": "ping"} | Replies "pong", so the app can test the connection from Settings |
| push | {"token", "action": "push", "table", "rows"} | Saves up to 500 rows by id: new rows are added, existing rows updated, and the newer updated_at wins. Sending the same rows twice never makes duplicates |
| pull | {"token", "action": "pull", "table", "since"} | Returns only rows changed after since, so the sync stays small as the sheet grows |
| onEdit | Nothing, it runs in the sheet | Stamps a fresh updated_at when you fix a row by hand, so every phone picks up the fix |
| setupDemoData | Nothing, you run it once from the editor | Creates the tabs and adds 5 demo customers (Client 1 to Client 5) with 15 sales and payments. Fixed ids, so a second run updates the same rows instead of adding copies |
| LockService | Nothing, it guards every push | One write at a time with one setValues call, so two phones syncing together cannot overwrite each other |
If you change the code later, use Deploy > Manage deployments, edit the existing deployment and pick New version. That keeps the same URL, so the app keeps working.
Step 3: Paste the full system prompt
Paste your web app URL and your token into the two boxes at the top of the prompt. The prompt fills itself in as you type, and a green line tells you when it is ready. Then press Copy and paste it into the Build box in AI Studio. The URL is remembered in this browser; the token is not saved anywhere. The full Code.gs is included at the end of the prompt on purpose: the agent reads the exact actions, field names and replies, and writes the app to match them instead of guessing.
Build a complete Android app called "ShopLedger" for a small shop owner
who sells on credit. Kotlin + Jetpack Compose, Material 3, a local Room
database, offline-first. Google Sheets is the cloud database and backup,
reached through the Apps Script web app given at the end.
SCREENS
1. PIN lock: set a 4-digit PIN on first launch, ask for it every time the
app opens.
2. Dashboard: today's sales, today's payments received, total receivable
from all customers, top 5 customers by balance due, last sync time and
a "Sync now" button.
3. Customers: list with search by name or phone, balance on every card
(red when money is due), filter "Only with dues", floating "+" button.
4. Add / Edit Customer: name (required), phone (required, digits only,
unique), opening balance (number, default 0), notes.
5. Customer detail: balance at the top, "Add Sale" and "Add Payment"
buttons, Call and WhatsApp buttons, transaction history newest first,
"Share statement" as plain text.
6. Add Sale / Add Payment: date (default today), amount (required,
greater than 0), note.
7. Transactions: every entry, filters Today / Last 7 days / This month
and type (sale or payment).
8. Settings: Web App URL, API token, "Test connection", "Sync now",
auto-sync on/off, last sync time, "Restore from Google Sheet",
change PIN, currency code (default USD, any ISO code
such as EUR, GBP, INR or PKR).
DATA (Room tables use the same field names as the Google Sheet)
- customers: id (UUID text), name, phone, opening_balance, notes,
updated_at, deleted
- transactions: id (UUID text), customer_id, date (YYYY-MM-DD),
type ("sale" or "payment"), amount, note, updated_at, deleted
- updated_at = ISO 8601 UTC time of the last change,
e.g. 2026-11-08T10:15:00Z
- deleted = true/false. Never remove a row: mark it deleted and hide it.
RULES
- balance = opening_balance + sum(sales) - sum(payments)
- amount must be greater than 0
- a customer who has transactions can be edited but not deleted
- every add, edit and delete sets updated_at to now and marks the row
"pending sync"
SYNC WITH GOOGLE SHEETS
- Every call: HTTP POST to the Web App URL, header
Content-Type: text/plain;charset=utf-8, JSON body, follow redirects.
- Test connection: {"token": TOKEN, "action": "ping"}
- Push: {"token": TOKEN, "action": "push", "table": "Customers" or
"Transactions", "rows": [...]} with up to 200 pending rows per call.
On "success": true, clear "pending sync" on those rows.
- Pull: {"token": TOKEN, "action": "pull", "table": "...",
"since": lastPullTime}. Save a returned row only when its updated_at is
newer than the local copy. Store the reply's "time" as the new
lastPullTime for that table.
- Order: push Customers, push Transactions, pull Customers, pull
Transactions.
- Run sync with WorkManager when the network is available: after every
change, every 15 minutes, and from "Sync now".
- On "success": false, keep the rows pending and show the message in
Settings.
- "Restore from Google Sheet" = pull both tables with since "".
DEFAULTS (editable in Settings)
Web App URL: PASTE_YOUR_WEB_APP_URL_HERE
API token: PASTE_THE_SAME_TOKEN_AS_API_TOKEN_IN_CODE_GS
DESIGN
Navy blue theme (#001f3f primary, #0074D9 accent), bottom navigation
(Dashboard, Customers, Transactions, Settings), large tap targets,
amounts right-aligned, readable on a 6-inch phone.
BACKEND
This Google Apps Script is already deployed as the web app above.
Do not change it. Only call it exactly as described.
/**
* Developed by Mohammad Rameez Imdad (Rameez Scripts)
* WhatsApp: https://wa.me/923435347417 (For Custom Projects)
* YouTube: https://www.youtube.com/@rameezimdad (Subscribe for more!)
*
* ShopLedger API - Google Sheet backend for the Android app
*/
const API_TOKEN = 'change-me-to-a-long-random-secret';
const TABLES = {
Customers: ['id', 'name', 'phone', 'opening_balance', 'notes', 'updated_at', 'deleted'],
Transactions: ['id', 'customer_id', 'date', 'type', 'amount', 'note', 'updated_at', 'deleted']
};
const isMoney = c => c === 'amount' || c === 'opening_balance';
// run once from the editor: creates both tabs
function setup() {
const ss = SpreadsheetApp.getActive();
Object.entries(TABLES).forEach(([name, cols]) => {
const sh = ss.getSheetByName(name) || ss.insertSheet(name);
sh.getRange(1, 1, 1, cols.length).setValues([cols]).setFontWeight('bold');
sh.setFrozenRows(1);
// text cols keep phone zeros + ISO dates as typed
cols.forEach((c, i) => sh.getRange(2, i + 1, sh.getMaxRows() - 1, 1)
.setNumberFormat(isMoney(c) ? '#,##0.00' : '@'));
});
}
function doGet() {
return out_({ success: true, message: 'ShopLedger API is running' });
}
function doPost(e) {
let req;
try { req = JSON.parse(e.postData.contents); }
catch (err) { return out_({ success: false, message: 'Bad JSON' }); }
if (req.token !== API_TOKEN) return out_({ success: false, message: 'Unauthorized' });
try {
if (req.action === 'ping') return out_({ success: true, message: 'pong', time: now_() });
if (req.action === 'push') return out_(push_(req.table, req.rows || []));
if (req.action === 'pull') return out_(pull_(req.table, String(req.since || '')));
return out_({ success: false, message: 'Unknown action' });
} catch (err) {
return out_({ success: false, message: String(err.message || err) });
}
}
// upsert by id, newer updated_at wins
function push_(table, rows) {
const cols = TABLES[table];
if (!cols) return { success: false, message: 'Unknown table' };
if (rows.length > 500) return { success: false, message: 'Max 500 rows per call' };
const lock = LockService.getScriptLock();
lock.waitLock(20000); // one write at a time
try {
const sh = SpreadsheetApp.getActive().getSheetByName(table);
const vals = sh.getDataRange().getValues().slice(1);
const idx = new Map(vals.map((r, i) => [String(r[0]), i]));
const u = cols.indexOf('updated_at');
let added = 0, updated = 0, skipped = 0;
rows.forEach(o => {
if (!o.id) return skipped++;
const line = cols.map(c => c === 'deleted' ? (o.deleted ? 'yes' : '')
: isMoney(c) ? Number(o[c]) || 0 : String(o[c] == null ? '' : o[c]));
const at = idx.get(String(o.id));
if (at === undefined) { idx.set(String(o.id), vals.length); vals.push(line); added++; }
else if (String(line[u]) >= String(vals[at][u])) { vals[at] = line; updated++; }
else skipped++; // sheet copy is newer
});
if (vals.length) sh.getRange(2, 1, vals.length, cols.length).setValues(vals);
return { success: true, added, updated, skipped };
} finally {
lock.releaseLock();
}
}
// rows changed after "since" (empty = all)
function pull_(table, since) {
const cols = TABLES[table];
if (!cols) return { success: false, message: 'Unknown table' };
const time = now_();
const u = cols.indexOf('updated_at');
const rows = SpreadsheetApp.getActive().getSheetByName(table).getDataRange().getValues().slice(1)
.filter(r => r[0] !== '' && String(r[u]) > since)
.map(r => Object.fromEntries(cols.map((c, i) => [c, c === 'deleted' ? r[i] === 'yes' : r[i]])));
return { success: true, rows, time };
}
// hand edits in the sheet get a fresh updated_at so phones pull them
function onEdit(e) {
const sh = e.range.getSheet(), cols = TABLES[sh.getName()];
if (!cols || e.range.getRow() < 2) return;
const u = cols.indexOf('updated_at') + 1;
if (e.range.getColumn() === u && e.range.getNumColumns() === 1) return;
sh.getRange(e.range.getRow(), u, e.range.getNumRows(), 1).setValue(now_());
}
const now_ = () => new Date().toISOString().replace(/\.\d+Z$/, 'Z');
const out_ = o => ContentService.createTextOutput(JSON.stringify(o))
.setMimeType(ContentService.MimeType.JSON);
// run from the editor: 5 demo customers + 15 entries, safe to run again (fixed ids)
function setupDemoData() {
setup();
const at = now_(), day = n => new Date(Date.now() - n * 864e5).toISOString().slice(0, 10);
const customers = [1, 2, 3, 4, 5].map(n => ({
id: 'DEMO-C' + n, name: 'Client ' + n, phone: '0300' + (1000000 + n),
opening_balance: n * 100, notes: 'House ' + n + ', Street ' + n + ', Demo City',
updated_at: at, deleted: false
}));
const tx = [];
customers.forEach((c, i) => [0, 1, 2].forEach(k => tx.push({
id: 'DEMO-T' + (i * 3 + k + 1), customer_id: c.id, date: day(i + k * 3),
type: k === 1 ? 'payment' : 'sale', amount: k === 1 ? 500 : (k + 1) * 1000,
note: 'Demo entry ' + (i * 3 + k + 1), updated_at: at, deleted: false
})));
Logger.log(JSON.stringify([push_('Customers', customers), push_('Transactions', tx)]));
}
| Prompt section | What it tells the agent |
|---|---|
| SCREENS | All 8 screens and what each one shows, so nothing is invented |
| DATA | Field names that match the sheet columns exactly |
| RULES | The balance formula and what must never happen |
| SYNC | The exact requests, their order, and what to do when a reply fails |
| DEFAULTS | Your web app URL and token, still editable in Settings |
| BACKEND | The full Code.gs, so the agent reads the real API instead of guessing it |
That structure is the difference between an app and a nice-looking mock-up.
- [ ] Paste your
/execweb app URL into the first box above the prompt. - [ ] Type the same token you set as
API_TOKENin Code.gs into the second box. - [ ] Wait for "Prompt ready", press Copy, paste it into the Build box in AI Studio and send it.
Step 4: Review the preview and refine one change at a time
The agent generates the project and opens it in the preview. Run through this list before you change anything:
- [ ] Set a 4-digit PIN and reopen the app: it asks for the PIN.
- [ ] Settings > Test connection shows success.
- [ ] Add two customers and a few sales and payments.
- [ ] The customer balance equals opening balance + sales - payments.
- [ ] Press Sync now: the rows appear in both tabs of your Google Sheet.
- [ ] Change a name in the sheet, press Sync now: the app shows the new name.
- [ ] Turn off the internet, add a sale, turn it back on: the sale reaches the sheet.
Then ask for changes in the chat panel, one at a time: "Add a monthly report screen" or "Show the customer's last payment date on the card". A single clear request is easier to check, and if it breaks something, you know which request broke it.
- Code tab: read or edit the generated files directly.
- The Android emulator runs in the cloud, so you can tap, scroll and rotate it. It does not support the camera, NFC or Bluetooth, GPS is simulated, and Google Play services features such as Google Sign-In and Maps only work on a real phone.
Step 5: Install the app on your phone
- [ ] On the phone, open Settings > About phone and tap Build number seven times to unlock Developer options.
- [ ] In Developer options, turn on USB debugging.
- [ ] Connect the phone to your computer with a USB cable and use Chrome or Edge.
- [ ] In the preview panel, click Install on Device and pick your phone from the browser's device list.
- [ ] Accept the "Allow USB debugging" prompt on the phone. The app installs and opens.
Each new build has to be installed again the same way. Building a web app instead? It deploys to Google Cloud Run and gets a public link; Gemini calls made by people using your shared app count against your own usage, and Cloud Run can bill once you pass the free allowance.
Step 6: Publish to Google Play or export the code
Open Settings > Publish and click Publish to Play Store. You need a Google Play developer account, which has a one-time fee of 25 US dollars. AI Studio signs the app and publishes it to the internal testing track, which allows up to 100 testers. A public release on the Play Store is still done by you in the Play Console, where the app goes through Google's review.
To keep building outside AI Studio, download the project as a ZIP from the Code tab and open it in Android Studio. GitHub export is not available for Android projects yet; web projects can sync with GitHub from Settings > GitHub.
Why the sync is built this way
- Save on the phone first, sync after. Each Apps Script call takes a second or more. The app saves to its own database at once and sends changes in batches in the background, so it keeps working without internet and the sheet becomes your live backup.
- Plain text body, follow redirects. Apps Script answers through a redirect. If the app's HTTP client does not follow it, every reply looks empty. Sending the JSON as
text/plainkeeps the same request working from a browser too. - Ids are made on the phone. Every record gets a UUID when it is created, so the app never waits for the sheet to hand out a number, and a retried sync updates the same row instead of adding a copy.
- Nothing is deleted. A deleted record is marked
deleted, so the deletion reaches every phone and the sheet keeps the history. - The token is a lock on the door, not a vault. A token inside an app file can be extracted. It stops strangers who find the URL; for an app with many staff, add a login that gives each user their own token.
Prefer not to use Google Sheets? The same app can store data in Cloud Firestore with Firebase Authentication for Google sign-in, which AI Studio sets up for you from the setup card in the chat or under Settings > Integrations. Sheets wins when the owner or an accountant wants to open, filter and share the data in a familiar sheet. Our comparison of Apps Script vs PHP/MySQL covers where each backend fits as the business grows.
What it costs
| Item | Cost |
|---|---|
| Building in Google AI Studio | Free within the usage quota |
| Google Sheets and Apps Script | Free with a Google account, within Apps Script's daily limits |
| Installing on your own phone | Free |
| Google Play developer account | 25 US dollars, one time, only to publish on Google Play |
| Web app on Cloud Run | Free allowance, then billed by Google Cloud |
Limits to know before you promise anyone an app
| Area | Web app | Android app |
|---|---|---|
| Code | Generated web project | Kotlin and Jetpack Compose only, one activity, one module |
| Server code and secrets | Yes | No, client-side only |
| Google Sheets and other Workspace APIs | Yes | No, use an Apps Script API |
| Firebase login and database | Yes | Yes |
| Devices | Any browser | Phones and tablets, no Wear OS or Android TV |
| Export | ZIP, GitHub | ZIP only |
| Publishing | Cloud Run link | Play internal testing, up to 100 testers |
Apps Script has its own limits too: a single call can run for at most six minutes, and only a limited number of calls run at the same moment. Batching the sync, as the prompt asks, keeps a small shop well inside them.
The bigger limit is not in the table. The AI builds what you describe, and it does not know what you forgot to describe. Business apps usually need user roles, an activity log of who changed what, validation on every form, and reports that match the accountant's numbers. Ask for each of these by name, then test them yourself with demo data before anyone relies on the app. The same gaps are why businesses move off plain spreadsheets in the first place.
Build it yourself, or start from a working system?
Google AI Studio is excellent for a prototype, a personal tool, or an app for a small team with simple rules. With the code and prompt above, you can get from idea to a synced app on your phone in an afternoon, and that is genuinely new.
When the app has to handle money for several staff, roles and approvals, or hundreds of records a day, starting from code that already runs a real business saves weeks of prompting and fixing. To see another phone-to-Sheet setup, look at our free QR code scanner that saves to Google Sheets from Android or the web. You can browse more free Apps Script and PHP source code, or see custom development if you want a system built around your own process.
Sources: Google's documentation, "Build apps in Google AI Studio" and "Build Android apps in Google AI Studio", and Google's announcement "Bring any idea to life: Google AI Studio at I/O 2026". Features, limits and prices are as documented in November 2026 and may change.