Google Sheets Container Check-In: Find Driver Appointments
Intro: Replace calls and radios with container-number lookup
If you’re trying to build a driver check-in screen in Google Sheets, you’ve likely already automated inbound appointment booking to some degree, but still rely on phone calls and radios for the actual driver arrival. As you keep asking “What’s the container number?” “What time is your appointment?” the line grows, and the front desk ends up opening the appointment sheet and typing numbers into the search box all day long.
To reduce that pain, this post walks through building a backend lookup function in Google Sheets + Apps Script that, given a container number, automatically finds today’s appointment. In other words, we’re building the core logic of the arrival check-in screen that will run on the driver’s phone. The code is structured so you can copy–paste and run it as-is, and I’ll explain where to put it and how to test it in a realistic workflow.
This post assumes you already have the basic inbound appointment structure set up (appointment sheet, SETTINGS, APPT_ymd_, APPT_hm_, loadSettings_, etc.). If not, you should first set up Create the Google Sheets Inbound Appointment System Structure — Basics Part 1 and then come back.
What the check-in lookup function needs to do
From the driver’s perspective, a Google Sheets–based warehouse check-in system is simple: enter the container number and tap [Search]. Everything after that should be handled by the system. The Apps Script check-in lookup logic we’ll build here does the following:
- Reads the appointment sheet (APPT_MAIN) for today’s date
- Performs a first-pass validation on the container number format (empty, absurdly short, etc.)
- Searches today’s appointments for a row that has that container number
- Returns one of these cases:
- Exactly 1 match: returns appointment time, door/slot, equipment type, status, etc.
- 0 matches: returns a reason and message like “No appointment for this number today”
- 2+ matches: returns a “duplicate appointment” status and message
We’ll also create getInitialDataForCheckIn(), which sends the initial information the check-in screen needs when it first loads (today’s date, notice text, business hours, time zone) in one shot. The actual web UI will be implemented separately with HTML + client-side code; this post focuses on the backend API-style Apps Script functions that UI will call.
Group check-in-related settings: constants for sheet/column positions
In real operations, sheet structure and notices change more often than script logic. If you move a column, rename a sheet, or change the notice text and have to touch code in multiple places each time, bugs are inevitable. To prevent that, we’ll group the values used by check-in into a CHK_CONFIG constant object.
Once that’s in place, if you add warehouses or change the sheet layout later, you’ll only need to update a few lines of config at the top and the entire lookup logic will keep working.
Step 1 — Define check-in config constants
This code collects the sheet name, column indices, and SETTINGS keys used by check-in in one place.
Paste it at the very top of Code.gs in Google Sheets → Extensions → Apps Script, under your existing constants.
After pasting, just save (⌘S or Ctrl+S). No need to run it.
// These column indices must mean **exactly what the appointment series fixed for APPT_MAIN**:
// A date | B start time | C equipment type | D door | E container | F carrier | G client | H remark
// I end time | J created at | K qty | L pallets | M booking ID - **there is no status column yet.**
// If even one index is off, the lookup searches for a container number in the equipment column.
const CHK_CONFIG = { // → Check-in configuration group
APPT_SHEET_NAME: 'APPT_MAIN', // → Appointment main sheet name
COL_DATE: 1, // → A: appointment date
COL_TIME: 2, // → B: start time
COL_EQUIP_TYPE: 3, // → C: equipment type
COL_DOOR: 4, // → D: door / yard slot
COL_CNTR: 5, // → E: container number
COL_CARRIER: 6, // → F: carrier
COL_CLIENT: 7, // → G: client
SETTINGS_CHECKIN_NOTICE_KEY: 'CHECKIN_NOTICE', // → Notice text settings key
SETTINGS_BUSINESS_HOURS_KEY: 'CHECKIN_HOURS' // → Business hours settings key
}; //
// Do **not** write a time zone string here. Reuse `APPT_TZ` from basics part 5 (America/New_York).
// Otherwise the screen reports one zone while the date is computed in another - and 'today'
// silently becomes the wrong day.To sanity-check: visually compare APPT_MAIN and these column definitions. If they match your actual layout and the functions that reference this config don’t error, you’re good.
Send initial data: getInitialDataForCheckIn
Instead of re-computing today’s date and fetching notice text every time the check-in screen needs it, it’s cleaner to fetch everything once from the server and reuse it on the front end. That’s what getInitialDataForCheckIn() does.
This function returns four things at once:
todayYmd: today’s date inYYYY-MM-DD, viaAPPT_ymd_()notice: the check-in notice text from the SETTINGS sheet (empty string if not set)businessHours: business hours string (e.g.,09:00~18:00, empty if not set)timezone: the time zone used for check-in - theAPPT_TZvalue from basics part 5 (America/New_York)
Details like holiday logic are better handled earlier in the reservation/calendar part. In this post, the scope is just basic info to show at the top of the screen.
Step 2 — Initial data provider function
This code returns today’s date, notice text, and business hours in one shot for the check-in web app when it first opens.
Paste it at the very bottom of Code.gs, after your existing functions.
Then save, choose getInitialDataForCheckIn from the dropdown, run it once, and grant permissions.
function getInitialDataForCheckIn() { // → Provide initial data for check-in
const tz = APPT_TZ; // → Series time zone (America/New_York)
const now = new Date(); // → Current time
const todayYmd = APPT_ymd_(now); // → Convert to 'YYYY-MM-DD'
const settings = loadSettings_(); // → Read from SETTINGS sheet
const noticeKey = CHK_CONFIG.SETTINGS_CHECKIN_NOTICE_KEY; // → Notice text key
const hoursKey = CHK_CONFIG.SETTINGS_BUSINESS_HOURS_KEY; // → Business hours key
const notice = settings[noticeKey] || ''; // → Empty string if no notice
const hours = settings[hoursKey] || ''; // → Empty string if no hours
const result = { // → Object to send to UI
todayYmd: todayYmd, // → Today’s date
notice: notice, // → Notice text
businessHours: hours, // → Business hours string
timezone: tz // → Time zone info
};
return result; // → Return to web app
}To verify: run getInitialDataForCheckIn in the Apps Script editor. If it finishes without error, that’s a first pass. For more detail, add and run this test helper:
function CHK_testInitialData() { // → Test initial data function
const data = getInitialDataForCheckIn(); // → Call function
Logger.log(JSON.stringify(data)); // → Log contents
}Run CHK_testInitialData and check the execution log. If you see something like {"todayYmd":"2026-08-16","notice":...}, it’s working as intended.
Find today’s appointment by container: getApptInfoByCntr
Now for the core of this Google Sheets arrival check-in Apps Script: the lookup function. It takes a container number entered by a driver or floor staff and returns today’s appointment from APPT_MAIN.
The logic:
- Trim whitespace and uppercase the input.
- If empty or too short, immediately return a format error.
- Read the appointment sheet in one batch and loop rows 2 through last.
- Normalize the date cell with
APPT_ymd_()whether it is a Date, a serial number, or a string, then compare. - Among rows matching today’s date, collect those whose container equals the input.
- Based on the number of matches:
- 0:
NOT_FOUND_TODAY - 1:
OK+ details - 2+:
DUPLICATE_CNTR
In production, you might enforce a stricter pattern (e.g., 4 letters + 7 digits) with regex. Here we keep it to length checks so you can extend it to your own rules later.
Step 3 — Appointment lookup by container number
This code searches for today’s appointment by container number and returns a structured result/reason object.
Paste it directly under getInitialDataForCheckIn in Code.gs.
Then save and use the test function CHK_testGetApptInfoByCntr_ below to verify with real data.
function getApptInfoByCntr(cntrRaw) { // → Find today’s appointment by container number
const ss = SpreadsheetApp.getActiveSpreadsheet(); // → Current spreadsheet
const sheet = ss.getSheetByName(CHK_CONFIG.APPT_SHEET_NAME);// → Appointment main sheet
if (!sheet) { // → Sheet not found
throw new Error('Could not find the appointment sheet.'); // → Configuration error
}
const todayYmd = APPT_ymd_(new Date()); // → Today as a string
const cntr = String(cntrRaw || '').trim().toUpperCase(); // → Normalize input
if (!cntr) { // → Empty input
return {
found: false,
reason: 'EMPTY_CNTR',
message: 'Please enter a container number.'
};
}
if (cntr.length < 4) { // → Too short to be valid
return {
found: false,
reason: 'INVALID_CNTR',
message: 'The container number format is not valid.'
};
}
const lastRow = sheet.getLastRow(); // → Last row index
if (lastRow < 2) { // → No data rows
return { // → Result object
found: false, // → Not found
reason: 'NO_DATA_TODAY', // → No appointments today
message: 'There are no appointments registered for today.'
};
}
const lastCol = sheet.getLastColumn(); // → Last column index
const rng = sheet.getRange(2, 1, lastRow - 1, lastCol); // → Read from row 2 through last
const values = rng.getValues(); // → 2D array of values
const matches = []; // → Matching rows for today
let hasOtherDate = false; // → Exists on other date flag
const dataErrorRows = []; // → Rows we could not read a date from
for (let i = 0; i < values.length; i++) { // → Loop through rows
const row = values[i]; // → Current row
const dateCell = row[CHK_CONFIG.COL_DATE - 1]; // → Date cell
const cntrCell = String(row[CHK_CONFIG.COL_CNTR - 1] || '').trim().toUpperCase(); // → Container
if (!cntrCell) { // → No container on this row
continue; // → Nothing to compare
}
// **Match the number first.** Reading the date first and skipping on failure means a
// corrupted date on the very row we want is reported as 'no booking today'.
if (cntrCell !== cntr) { // → Different container
continue; // → Skip
}
// From here this **is** the row we were looking for. Never skip it silently.
let rowYmd; // → Row date string
try { // → Normalize
rowYmd = (dateCell instanceof Date) // → Date, serial or string
? APPT_ymd_(dateCell) // → 'YYYY-MM-DD'
: APPT_ymd_(String(dateCell).trim()); // → handles padding too
} catch (e) { // → Not a date
dataErrorRows.push(i + 2); // → A person must look at it
continue; //
} //
if (rowYmd === todayYmd) { // → This row is for today
matches.push({ // → Add to matches
rowIndex: i + 2, // → Actual row index in sheet
row: row // → Entire row data
});
} else { // → Same container, other date
hasOtherDate = true; // → Mark as existing on other date
}
}
// If exactly one booking was found today, that is definitive. Otherwise, if any row
// for this container had an unreadable date, do not claim there is no booking.
if (matches.length !== 1 && dataErrorRows.length > 0) { // → Cannot decide
return {
found: false,
reason: 'DATA_ERROR',
message: 'The date on this container\'s booking row could not be read (rows ' +
dataErrorRows.slice(0, 10).join(', ') + '). Please contact the office.'
};
}
if (matches.length === 0) { // → No match for today
if (hasOtherDate) { // → But exists on other dates
return {
found: false,
reason: 'OTHER_DATE',
message: 'This container number has an appointment, but on a different date. Please check the appointment date.'
};
}
return {
found: false,
reason: 'NOT_FOUND_TODAY',
message: 'There is no appointment for today with the entered container number.'
};
}
if (matches.length > 1) { // → Multiple matches found
return {
found: false,
reason: 'DUPLICATE_CNTR',
message: 'There are multiple appointments for today with this container number. Please check with the office.'
};
}
const match = matches[0]; // → Single matching row
const row = match.row; // → Row data
const timeCell = row[CHK_CONFIG.COL_TIME - 1]; // → Time cell
const equipType = row[CHK_CONFIG.COL_EQUIP_TYPE - 1] || ''; // → Equipment type
const door = row[CHK_CONFIG.COL_DOOR - 1] || ''; // → Door/slot
const carrier = row[CHK_CONFIG.COL_CARRIER - 1] || ''; // → Carrier
const client = row[CHK_CONFIG.COL_CLIENT - 1] || ''; // → Client
let timeStr = ''; // → Time as string
if (timeCell instanceof Date) { // → If Date object
timeStr = APPT_hm_(timeCell); // → Convert to 'HH:mm'
} else if (timeCell) { // → If there is a value
timeStr = String(timeCell); // → Use as string
}
return { // → Success result
found: true, // → Found
reason: 'OK', // → Normal
message: 'Appointment found.', // → Message
data: { // → Details
rowIndex: match.rowIndex, // → Row index
date: todayYmd, // → Date
time: timeStr, // → Time
container: cntr, // → Container number
equipType: equipType, // → Equipment type
door: door, // → Door/slot
carrier: carrier, // → Carrier
client: client // → Client
}
};
}To verify, plug in a real container number that exists, plus ones that don’t, using the test helper below. Check that the reason and message fit each case.
function CHK_testGetApptInfoByCntr_() { // → Test container lookup
const existingCntr = 'TESTCNTR1'; // → Replace with a real existing number
const missingCntr = 'NO_SUCH_CNTR'; // → Non-existent number
const otherDateCntr = 'OTHER_DATE_CNTR'; // → Number that exists only on another date
const res1 = getApptInfoByCntr(existingCntr); // → Lookup existing
const res2 = getApptInfoByCntr(missingCntr); // → Lookup non-existent
const res3 = getApptInfoByCntr(otherDateCntr);// → Lookup other-date-only
Logger.log('EXISTING: ' + JSON.stringify(res1)); // → Log results
Logger.log('MISSING: ' + JSON.stringify(res2)); // → Log results
Logger.log('OTHER_DATE: ' + JSON.stringify(res3));// → Log results
}Run CHK_testGetApptInfoByCntr_ and check the logs:
EXISTING should show found:true, reason:"OK",
MISSING should show found:false, reason:"NOT_FOUND_TODAY",
OTHER_DATE should show found:false, reason:"OTHER_DATE".
If so, it’s behaving as designed.
Common real-world mistakes and checkpoints
From rolling out similar Google Sheets container-number lookup automations at several warehouses, the same issues keep coming up:
First, forgetting the date condition. If you search for a container across the entire sheet without limiting to today, past and future appointments get pulled in, and trucks that shouldn’t be here today look “checked in.” That’s why this code only adds rows to matches when rowYmd === todayYmd, and uses hasOtherDate just to flag that the number exists on other dates.
Second, not validating container format at all. If you allow blanks, single letters like A, or 123 to go through, every “no result” case turns into back-and-forth between driver and desk to re-read the number. Tailor stricter checks (minimum length, alphabet/number pattern) to your local rules to cut down on that friction.
Third, mixed date formats. It’s common to have both true date-type cells and hand-typed string dates. That’s why we branch on instanceof Date and normalize via APPT_ymd_(), while string cells are compared as-is. Ideally, you also standardize how dates are saved in the appointment creation flow. For that, see How to Standardize Date Formats in Google Sheets: Inbound Appointment System Basics Part 5.
Fourth, no policy for duplicate appointments. In principle, a single container should have only one appointment. You should ideally block duplicates at save time. Using LockService and duplicate checks on save is covered in Preventing Duplicate Appointments in Google Sheets: Safely Saving with LockService. Still, legacy data or manual bookings can create duplicates, so the lookup layer explicitly returns DUPLICATE_CNTR so staff can recognize the situation immediately.
Wrap-up: Stabilize the lookup logic first, then build the UI
In this post, we implemented two core backend pieces for Google Sheets container-number check-in:
getInitialDataForCheckIn() and getApptInfoByCntr() in Apps Script.
- One function is called once when the check-in screen loads, to get today’s date, notice text, and business hours.
- The other takes a container number from the driver or staff, finds today’s appointment, and returns both structured data and a clear reason code.
Once these are stable, wiring them into a doGet(e)-based web app, recording check-in time/agent/status, and managing concurrent writes with LockService becomes much easier.
A concrete next step: pick a real container number from APPT_MAIN, plug it into CHK_testGetApptInfoByCntr_, and run it. Once you see the correct time, door, and status printed in the logs, you can move on to building the web UI with much more confidence.