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 glanceDetails
What you buildShopLedger: customers, sales and payments on Android, synced to Google Sheets
ToolsGoogle AI Studio (Build mode), Google Sheets, Google Apps Script
Coding neededNo. You copy the code and the prompt from this page
CostFree to build and test. 25 US dollars one time only if you publish on Google Play
TimeAbout 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.

Diagram: the six steps to build an app in Google AI Studio with a Google Sheets backend, from picking a platform to publishing
The six steps we follow in this guide.

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:

  1. The screens. Login, dashboard, a list, an add form, a detail view, settings. Name each one.
  2. The data. For each record, the fields and their types: name (text), phone (text), amount (number), date (date), type (sale or payment).
  3. 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

  1. [ ] Go to aistudio.google.com and sign in with your Google account.
  2. [ ] Click Build in the left-hand menu.
  3. [ ] 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

Diagram: Android app built in Google AI Studio sends JSON to an Apps Script web app, which reads and writes rows in a Google Sheet
An Apps Script web app connects the Android app to the Google Sheet.
  1. [ ] Create a new Google Sheet and name it ShopLedger Data.
  2. [ ] Open Extensions > Apps Script, delete the sample code in Code.gs, and paste the full code below.
  3. [ ] Change API_TOKEN to your own long random secret, for example 30 mixed letters and numbers. Keep a copy; the prompt needs it.
  4. [ ] Pick setup in the function list at the top and click Run. Allow the permissions. It creates the Customers and Transactions tabs with their headers.
  5. [ ] 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.
  6. [ ] Click Deploy > New deployment, choose type Web app, set Execute as to Me and Who has access to Anyone, then click Deploy.
  7. [ ] 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

TabColumnsNotes
Customersid, name, phone, opening_balance, notes, updated_at, deletedPhone is stored as text, so the leading zero stays
Transactionsid, customer_id, date, type, amount, note, updated_at, deletedtype 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

PartThe app sendsWhat 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
onEditNothing, it runs in the sheetStamps a fresh updated_at when you fix a row by hand, so every phone picks up the fix
setupDemoDataNothing, you run it once from the editorCreates 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
LockServiceNothing, it guards every pushOne 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 sectionWhat it tells the agent
SCREENSAll 8 screens and what each one shows, so nothing is invented
DATAField names that match the sheet columns exactly
RULESThe balance formula and what must never happen
SYNCThe exact requests, their order, and what to do when a reply fails
DEFAULTSYour web app URL and token, still editable in Settings
BACKENDThe 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 /exec web app URL into the first box above the prompt.
  • [ ] Type the same token you set as API_TOKEN in 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

  1. [ ] On the phone, open Settings > About phone and tap Build number seven times to unlock Developer options.
  2. [ ] In Developer options, turn on USB debugging.
  3. [ ] Connect the phone to your computer with a USB cable and use Chrome or Edge.
  4. [ ] In the preview panel, click Install on Device and pick your phone from the browser's device list.
  5. [ ] 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/plain keeps 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

ItemCost
Building in Google AI StudioFree within the usage quota
Google Sheets and Apps ScriptFree with a Google account, within Apps Script's daily limits
Installing on your own phoneFree
Google Play developer account25 US dollars, one time, only to publish on Google Play
Web app on Cloud RunFree allowance, then billed by Google Cloud

Limits to know before you promise anyone an app

AreaWeb appAndroid app
CodeGenerated web projectKotlin and Jetpack Compose only, one activity, one module
Server code and secretsYesNo, client-side only
Google Sheets and other Workspace APIsYesNo, use an Apps Script API
Firebase login and databaseYesYes
DevicesAny browserPhones and tablets, no Wear OS or Android TV
ExportZIP, GitHubZIP only
PublishingCloud Run linkPlay 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.