Blog / Construction Operations
Construction Daily Report with Google Forms (Photos, Weather, Manpower)
By Keyona Meeks, Founder, ReRev Labs · Oct 31, 2026 · 11 min read
The short answer: Make a Google Form with the fields your daily report needs: date, project, weather, crew counts, equipment, work performed, delays, visitors and photos. Send the answers to a Google Sheet, which becomes your log. A short Google Apps Script then saves a PDF of each report to a Drive folder, emails it to the person who needs it and to you, and each weekday afternoon emails you a list of projects that have not filed. This tracks what happened on site. Your contract decides what a daily report must contain.
By the end of this post, you'll be able to:
- Build a daily report form that works on a phone.
- Collect photos with the report and keep them in Drive.
- Install a script that saves a PDF per report and emails it.
- Get an alert when a project has no report for the day.
The superintendent writes the day down in a notebook, a text thread or a note on the phone. The notes are good. They are also in four places. When the owner asks what the weather was on the day of the pour, or who was on site the week of the delay, someone spends an hour digging.
A form fixes the collecting. It asks the same questions every day, on the phone, in a minute or two. A sheet and a script fix the finding and the nagging. The results I saw for this search on October 30, 2026 were mostly form-template pages and field-app pages. This post adds the part after the form: the PDF, the email and the missing-report alert.
I sell build work, so you should know I benefit if you hire help. The whole setup is below for free.
What does a daily report need to contain?
Your contract and your project requirements decide that. For a sense of what a written form looks like, here is one public owner's example. Washington State DOT publishes a "Project Diary" form (DOT Form 422-004LP, revised 06/2026 on the page I read). Its fields include "Weather," with separate AM and PM entries, a "Work Activity Summary," "Contractor's Workforce," "Contractor's Equipment," traffic control questions, and "Photos/Videos taken Today?" Source: WSDOT Project Diary, Form 422-004LP, checked October 30, 2026.
That is a public owner's diary for its inspectors on a highway job, not a contractor's report, and it is one example. Your owner, your contract or your insurer may ask for different fields. Read them first, then build the form to match.
What questions should the form ask?
Keep it short enough to finish on a phone. A starting set:
| Question | Type | Notes |
|---|---|---|
| Report date | Date | |
| Project | Dropdown | One choice per active project |
| Reported by | Short answer | |
| Weather | Dropdown | Clear, Cloudy, Rain, Snow, Wind, Other |
| Own crew count | Short answer | A number |
| Subcontractors on site (name and headcount) | Paragraph | One per line |
| Equipment on site | Paragraph | |
| Work performed today | Paragraph | |
| Delays or problems | Paragraph | |
| Visitors and inspections | Paragraph | |
| Photos | File upload | See below |
The script finds each answer by its question title, so the titles in the script must match the titles in your form exactly. If you change a title, change it in the script too.
How do you collect photos?
Use a File upload question. Google's help page says "To answer this question, responders need to sign in to a Google Account," and that "For the form owner, uploaded files are stored in a new folder on Google Drive." It also says you set the maximum number of files and the maximum file size. The page also says you cannot use this question type if the form is stored in a shared drive or if your administrator turns on Data Loss Prevention. Source: Google's help page on question types, checked October 30, 2026.
Two things follow. Everyone who files a report needs a Google account, and the photos land in the form owner's Drive. If your crew will not sign in, drop the File upload question and ask for a link instead. The sheet will record the Drive links to the photos. The PDF in this post lists those links. It does not embed the pictures.
How do you send the answers to a sheet?
Google's help page gives the steps. In Google Forms: open your form, click Responses, then the Summary tab. At the top right, click More, then Select destination for responses. Choose Create a new spreadsheet or Select existing spreadsheet, then click Create or Select. Source: Google's help page on choosing where to save form responses, checked October 30, 2026.
Anyone who can open the sheet can read every report. The help page says later permission changes on the form do not carry over to the sheet, so set the sheet's sharing on purpose.
How does a script make a PDF per report and warn you about missing ones?
The script has two jobs. When a report arrives, it builds a one-page document from the answers, saves it as a PDF in a Drive folder, writes the PDF link in the sheet and emails the PDF to the person for that project. Each weekday afternoon, it checks which active projects have no report dated today and emails you the list.
Apps Script can run a function each time a form response lands in the sheet. Google's docs describe the event the function receives: namedValues is "An object containing the question names and values from the form submission," and range is "A Range object, representing the cell or range of cells that were edited." Source: Event Objects in Google's Apps Script guide, checked October 30, 2026.
The script needs installable triggers, because creating files and sending email need authorization. Google's docs say installable triggers "can call services that require authorization" and "always run under the account of the person who created them." Source: Installable Triggers, checked October 30, 2026. So the PDFs and emails come from the account that installs the triggers.
If you have not used Apps Script before, read what Google Apps Script is first.
Set up the sheet
In the response sheet, make a tab called Projects. Put Project in A1 and Email in B1. List each active project name in column A, spelled exactly as in the form dropdown. Put the email address that should receive that project's reports in column B. Make a folder in Drive for the PDFs, open it, and copy the long ID from the end of its web address.
Install the script
- In the response sheet, click Extensions, then Apps Script.
- Delete what is in the editor and paste the code below.
- Change the settings at the top: the folder ID, your email and the question titles.
- Click Save.
- In the function list, choose
setupDailyReportTriggersand click Run. Google asks you to allow the script. Read what it asks for and approve it if you agree.
// Turn each daily report form response into a PDF and check for missing reports.
// Paste into Extensions > Apps Script in the Google Sheet that receives your form responses.
const CONFIG = {
RESPONSES_SHEET: 'Form Responses 1',
PROJECTS_SHEET: 'Projects', // A2 down = active project names, B2 down = who gets that project's reports
PDF_FOLDER_ID: 'PASTE_YOUR_DRIVE_FOLDER_ID',
OWNER_EMAIL: 'you@yourcompany.com', // gets a copy of each report and the missing report list
EMAIL_OWNER: true, // false = stop the owner copies when you stop overseeing
EMAIL_PROJECT_RECIPIENT: true, // true = email each report to the address on the Projects tab
DATE_QUESTION: 'Report date', // each name below must match a form question title exactly
PROJECT_QUESTION: 'Project',
FIELDS: [ // printed on the PDF, in this order
'Report date', 'Project', 'Reported by', 'Weather', 'Own crew count',
'Subcontractors on site (name and headcount)', 'Equipment on site',
'Work performed today', 'Delays or problems', 'Visitors and inspections', 'Photos'
]
};
function onDailyReportSubmit(e) {
const sheet = e.range.getSheet();
const row = e.range.getRow();
const answers = e.namedValues;
const get = function (q) {
return answers[q] && answers[q][0] ? String(answers[q][0]).trim() : '';
};
const pdfCol = ensureColumn_(sheet, 'PDF link');
if (sheet.getRange(row, pdfCol).getValue() !== '') return;
const project = get(CONFIG.PROJECT_QUESTION) || '(no project)';
const dateText = get(CONFIG.DATE_QUESTION) || 'no date';
const title = 'Daily report - ' + project + ' - ' + dateText.replace(/\//g, '-');
const doc = DocumentApp.create(title);
const body = doc.getBody();
body.appendParagraph(title).setHeading(DocumentApp.ParagraphHeading.HEADING1);
CONFIG.FIELDS.forEach(function (q) {
body.appendParagraph(q).setHeading(DocumentApp.ParagraphHeading.HEADING3);
body.appendParagraph(get(q) || '(blank)');
});
doc.saveAndClose();
const blob = DriveApp.getFileById(doc.getId()).getAs('application/pdf').setName(title + '.pdf');
const pdf = DriveApp.getFolderById(CONFIG.PDF_FOLDER_ID).createFile(blob);
DriveApp.getFileById(doc.getId()).setTrashed(true);
sheet.getRange(row, pdfCol).setValue(pdf.getUrl());
const to = CONFIG.EMAIL_PROJECT_RECIPIENT ? reportRecipient_(project) : '';
const ownerCopy = CONFIG.EMAIL_OWNER ? CONFIG.OWNER_EMAIL : '';
const recipients = [to, ownerCopy].filter(function (a, i, all) {
return a && all.indexOf(a) === i;
});
if (recipients.length) {
MailApp.sendEmail({
to: recipients.join(','),
subject: title,
body: 'Daily report attached. A copy is saved in Drive: ' + pdf.getUrl(),
attachments: [pdf.getBlob()]
});
}
}
function reportRecipient_(project) {
const sheet = SpreadsheetApp.getActive().getSheetByName(CONFIG.PROJECTS_SHEET);
const rows = sheet.getDataRange().getValues();
for (let i = 1; i < rows.length; i++) {
if (String(rows[i][0]).trim() === project) return String(rows[i][1]).trim();
}
return '';
}
function checkMissingReports() {
const today = new Date();
const day = today.getDay();
if (day === 0 || day === 6) return; // weekdays only
const ss = SpreadsheetApp.getActive();
const projects = ss.getSheetByName(CONFIG.PROJECTS_SHEET).getDataRange().getValues().slice(1)
.map(function (r) { return String(r[0]).trim(); })
.filter(function (p) { return p !== ''; });
const data = ss.getSheetByName(CONFIG.RESPONSES_SHEET).getDataRange().getValues();
const col = headerMap_(data[0]);
const todayKey = dayKey_(today);
const filed = {};
for (let r = 1; r < data.length; r++) {
const d = data[r][col[CONFIG.DATE_QUESTION]];
if (d instanceof Date && dayKey_(d) === todayKey) {
filed[String(data[r][col[CONFIG.PROJECT_QUESTION]]).trim()] = true;
}
}
const missing = projects.filter(function (p) { return !filed[p]; });
if (missing.length === 0 || !CONFIG.EMAIL_OWNER) return;
MailApp.sendEmail({
to: CONFIG.OWNER_EMAIL,
subject: 'No daily report yet: ' + missing.length + ' project(s)',
body: missing.join('\n')
});
}
function dayKey_(d) {
return d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate();
}
function ensureColumn_(sheet, header) {
const lastCol = sheet.getLastColumn();
const headers = sheet.getRange(1, 1, 1, lastCol).getValues()[0];
const i = headers.map(function (h) { return String(h).trim(); }).indexOf(header);
if (i !== -1) return i + 1;
sheet.getRange(1, lastCol + 1).setValue(header);
return lastCol + 1;
}
function headerMap_(headers) {
const map = {};
headers.forEach(function (h, i) { map[String(h).trim()] = i; });
return map;
}
function setupDailyReportTriggers() {
ScriptApp.getProjectTriggers().forEach(function (t) {
const f = t.getHandlerFunction();
if (f === 'onDailyReportSubmit' || f === 'checkMissingReports') ScriptApp.deleteTrigger(t);
});
ScriptApp.newTrigger('onDailyReportSubmit')
.forSpreadsheet(SpreadsheetApp.getActive()).onFormSubmit().create();
ScriptApp.newTrigger('checkMissingReports').timeBased().everyDays(1).atHour(16).create();
}
On its first run for a row, the script adds a PDF link column to the end of the sheet if it is missing. If that cell already has a value, the script skips the row, so a re-run does not make a second PDF or send a second email.
What the script does
- Builds the PDF. It creates a temporary document with one heading per field, converts it to a PDF, saves the PDF in your folder, and moves the temporary document to the trash.
- Emails the PDF. It sends the PDF to the address on the
Projectstab for that project and toOWNER_EMAIL, in one message. If the project is not on the tab, it still makes the PDF and sends it toOWNER_EMAILonly. If the address is the same in both places, it sends one copy. - Prints blanks as blanks. An unanswered field shows as
(blank), so a missing answer is visible. - Checks for missing reports. On weekdays at about 4 PM in the script's time zone, it lists active projects with no report dated today and emails you. It sends nothing if every project has filed, and nothing on weekends.
How do you test it before you trust it?
I tested the script with made-up responses in a mock harness and a syntax check. I did not run it in a live Google account. Test it on a copy of the form and sheet:
- Add one project to the
Projectstab with your own email address. - Submit the form once for that project.
- Check that the sheet row gets a
PDF link, that a PDF is in your folder and that the email arrived with the PDF attached. - Open the PDF and check every field against your answers.
- Submit the same form again for the same date. You should get a second PDF, because each submission is a report. Edit the response in the sheet only if you mean to.
- Add a second project to the
Projectstab and do not file for it. RuncheckMissingReportsby hand on a weekday and check that the email lists it. - Open Executions in the Apps Script editor if anything fails, and read the error.
Things I could not confirm without a live account: that e.range points to the new row in every case, how the PDF looks when a paragraph answer is long, whether the Report date answer matches the date the script checks in every time zone, and how the file upload links appear in the sheet. Test those on your own copy.
Where does an LLM fit in this?
Not in the form, the PDF or the missing-report check. Those are rules. An LLM can help in one place: reading a long, messy "Work performed today" note and suggesting a short summary line for the weekly report. A person should read the original and the summary before anyone relies on it. A daily report can end up as a record in a dispute, so the words should be the superintendent's. The stance I use: AI writes the code, plain automation runs it, and an LLM shows up only where the input is messy and a person checks the result.
Where this has limits
- This is tracking, not advice. The form does not tell you what your contract requires in a daily report, who must receive it or how long to keep it. Check your contract, and ask an attorney if you are unsure.
- It does not record safety incidents. A daily report form is not an injury or incident report. OSHA's recordkeeping rule says its purpose "is to require employers to record and report work-related fatalities," "injuries, and illnesses." Source: 29 CFR 1904.0, Purpose, checked October 30, 2026. That is a federal example. Your reporting duties depend on your work and your state. Check with a safety professional.
- Reports can be edited. Anyone with edit access to the sheet can change an answer after the PDF is made, and the PDF will not change. Limit who can edit, and treat the PDF as the record of what was filed.
- Photos need sign-in. The File upload question requires a Google account. See the section on photos.
- The PDF is plain. It lists the answers and photo links. It does not embed pictures or follow your company template.
- One report per submission. If two people file for the same project and date, you get two PDFs.
- The script is untested in a live account. Test it on a copy first.
- Quotas apply. Apps Script limits how many emails a script can send each day and how long it can run. See Google's quotas page for the current numbers.
- One owner. If the script fails, PDFs and alerts stop. Check Executions in Apps Script now and then, and name one person who owns it.
- Software exists for this. Field apps built for daily reports handle photos, signatures and templates more smoothly. A form fits a small team that wants a simple log.
If your daily reports, photos and field notes live in texts and notebooks, see our AI-powered operations work or apply on /letstalk. Bring a sample report with names removed and a note on where reports get lost today. Do not send photos of the site, contracts, passwords or login details.
To decide what to fix first, read what to automate first in a small business.
Frequently asked questions
Can I make a construction daily report with Google Forms?
Yes. Build the form with the fields your contract or owner asks for, send the answers to a Google Sheet, and use the script above if you want a PDF per report and a missing-report alert.
Will the field crew use it?
A form on the phone with dropdowns is quicker than writing a page. It still depends on the habit. The missing-report email is there so the habit has a backstop. If the crew will not sign in to a Google account, you cannot use the photo upload question.
Can the form capture photos?
Yes, with a File upload question. Responders must sign in to a Google Account, and the files go to a folder in the form owner's Drive. The PDF lists the photo links and does not embed the photos.
Does the PDF count as the official daily report?
That depends on your contract and your owner. The PDF is a copy of what was filed in the form. Ask your contract administrator or attorney what counts.
Can the report go to the owner automatically?
In this script, OWNER_EMAIL is you, the person running it, not the building owner. If the building owner should get the report, put that address on the Projects tab. By default, two groups get email. The address you put on the Projects tab for each project gets that project's PDF, and OWNER_EMAIL, which is you, gets a copy of every report and the missing-report list. Tell the project recipients before you turn it on, and check a few PDFs by hand first. When you are ready to stop overseeing each report, set EMAIL_OWNER: false. The project recipients still get their PDFs, and the missing-report email stops. That is your choice, and you can set it back at any time. To send reports to yourself only while you test, set EMAIL_PROJECT_RECIPIENT: false.
What if there is no work on a day?
The script checks weekdays only and cannot tell a rain day from a missed report. File a short report that says no work, or expect the email and ignore it.
Can I get a weekly summary?
Not in this post. A weekly summary is a different script that reads the sheet once a week and sends one list.
Read next: The weekly owner report rolls daily reports like this one into the summary the owner sees, and the paperwork map shows where field reports meet office logs.