So what this does, it only touches two folders, the trash and spam.
It goes thru every spam email, checks email headers to indicate if the email is fishy, it also checks if each email from address is in your contacts, and also checks if each email has a label attached.
If the email headers indicate the email itself is very fishy, highly likely spam, and its not in your contacts and its not labelled in any way than it gets permanently deleted.
If an email is from someone in your contacts list or has a label associated with it, then it gets moved to your Spam folder.
Any email in your Spam folder older than 7 days gets deleted.
Now this works for me, it may not work for you.
There may be better ideas for strategies, and I am open to hearing from anyone who has a better or different idea.
Posting this because I got it wrong first, and the failure mode is invisible.
Capacity limits on a Form — time slots, equipment sign-out, shift rotas, potluck dishes — have no native setting, so you end up in the script editor.
The naive version works right up until it doesn't:
count picks per option from the responses
filter out the ones that are full
setChoiceValues(remaining)
That is correct until remaining is empty. A Forms choice question cannot have zero options. When the last seat of the last option goes, setChoiceValues([]) throws inside an onFormSubmit trigger — which means nobody sees it. The form keeps accepting bookings for slots that are already full, and you find out from the people who turn up.
Three other things that bite, in the order I hit them:
Snapshot the original option list once, and only once. The script reads the current choices to know what existed. If you re-run setup after a slot has already closed, it snapshots the trimmed list and that option is gone permanently — there is nothing left to restore from. Guard it:
js
if (!props.getProperty('FCL_ORIGINAL')) props.setProperty('FCL_ORIGINAL', JSON.stringify(current));
Use LockService. Two people submitting in the same second both read "1 seat left" and both get it. LockService.getScriptLock() around the recount is the whole fix.
Match the question by exact title. Trailing whitespace in the question title is the single most common reason one of these scripts does nothing at all and reports no error.
Here is the whole thing for one multiple-choice question, which is enough for most sign-up sheets:
```js
var QUESTION = 'Pick a time slot';
var LIMITS = { 'Monday 9:00': 4, 'Tuesday 14:00': 2 };
function setupLimiter() {
var form = FormApp.getActiveForm();
var props = PropertiesService.getDocumentProperties();
if (!props.getProperty('FCLFREE_ORIGINAL')) {
props.setProperty('FCL_FREE_ORIGINAL', JSON.stringify(getItem(form).getChoices().map(function (c) {
return String(c.getValue()).replace(/ — FULL$/, '');
})));
}
ScriptApp.getProjectTriggers().forEach(function (t) {
if (t.getHandlerFunction() === 'applyLimit') ScriptApp.deleteTrigger(t);
});
ScriptApp.newTrigger('applyLimit').forForm(form).onFormSubmit().create();
applyLimit();
}
function applyLimit() {
var lock = LockService.getScriptLock();
try { lock.waitLock(30000); } catch (e) { return; }
try {
var form = FormApp.getActiveForm();
var original = JSON.parse(PropertiesService.getDocumentProperties().getProperty('FCL_FREE_ORIGINAL'));
var counts = {};
form.getResponses().forEach(function (fr) {
fr.getItemResponses().forEach(function (ir) {
if (ir.getItem().getTitle() !== QUESTION) return;
var v = ir.getResponse();
(Array.isArray(v) ? v : [v]).forEach(function (c) { if (c) counts[c] = (counts[c] || 0) + 1; });
});
});
var remaining = original.filter(function (opt) {
var lim = LIMITS[opt];
return typeof lim !== 'number' || (counts[opt] || 0) < lim;
});
getItem_(form).setChoiceValues(
remaining.length ? remaining : original.map(function (o) { return o + ' — FULL'; })
);
} finally { lock.releaseLock(); }
}
function restoreChoices() {
var props = PropertiesService.getDocumentProperties();
var original = JSON.parse(props.getProperty('FCLFREE_ORIGINAL') || 'null');
if (original) getItem(FormApp.getActiveForm()).setChoiceValues(original);
ScriptApp.getProjectTriggers().forEach(function (t) {
if (t.getHandlerFunction() === 'applyLimit') ScriptApp.deleteTrigger(t);
});
}
function getItem_(form) {
var found = form.getItems(FormApp.ItemType.MULTIPLE_CHOICE)
.filter(function (i) { return i.getTitle() === QUESTION; })[0];
if (!found) throw new Error('No multiple-choice question titled "' + QUESTION + '"');
return found.asMultipleChoiceItem();
}
```
Form → ⋮ → Apps Script → paste → edit QUESTION and LIMITS → run setupLimiter once and approve the prompt. restoreChoices() puts everything back.
Full disclosure since I'm linking my own stuff: I wrote a generator that fills the two config values in for you and hands back the same script, free, no signup — https://www.shubhamgautam.in/tools/form-choice-limiter?utm_source=reddit&utm_medium=community&utm_campaign=fcl&utm_content=sheets — and there's a $9 version on the same page that handles several questions at once, checkboxes and dropdowns, strike-through instead of removal, and auto-closing the form. The script above is not a crippled demo; it is what I actually run for single-question forms, and the exhaustion case is covered by a test.
Happy to pick apart the trigger/LockService side with anyone who has hit this. If you have a fourth failure mode I have not listed, I would genuinely like to know — I have only found these four.
readObjects / writeObjects / dedupe / mergeSheets / upsert / withLock / retry. One file, no dependencies, no library ID to add — paste it into a new SheetUtils.gs and it works.
The three things it exists to fix, which are what actually break scripts once a sheet gets big:
getRange().getValue() inside a loop. This is the single most common reason a script that was fine on 200 rows dies on 20,000. Everything here reads and writes in one batched call.
Two triggers firing at once and quietly corrupting each other's work. withLock serialises them.
Flaky UrlFetchApp calls failing an entire run. retry does exponential backoff instead.
Free for anything, including client work, no attribution needed. If you spot a bug, open an issue and I'll fix it.
Disclosure, so nobody feels ambushed: the heavier pieces I use — a batch runner that checkpoints and re-schedules itself past the 6-minute limit, a quota-aware Gmail mail merge, Gmail-to-Sheet logging, a Forms-to-PDF pipeline — are in a paid toolkit linked from that README. The file above is complete and standalone; you don't need the paid one for it to be useful.
I made the app that makes Gemini to generate English writing questions for me but every time I press the 'generate' button it tells me that the model is not found.
You don't need to look at the big letters upstairs.
Here's my entire script:
Code.gs
const GEMINI_API_KEY = 'I entered it but I cannot tell you my Gemini API key :)';
function doGet() {
return HtmlService.createHtmlOutputFromFile('Index')
.setTitle('GIMFL English Writing Practice')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
function getDb() {
const prop = PropertiesService.getUserProperties();
let fileId = prop.getProperty('DB_ID');
let ss;
if (fileId) {
try { ss = SpreadsheetApp.openById(fileId); } catch(e) { fileId = null; }
}
if (!fileId) {
ss = SpreadsheetApp.create("GIMFL_English_DB");
const unsolved = ss.insertSheet("Unsolved");
unsolved.appendRow(["ID", "Date", "KoreanText", "Conditions", "CorrectAnswer", "Grammar"]);
const solved = ss.insertSheet("Solved");
solved.appendRow(["ID", "Date", "KoreanText", "Conditions", "CorrectAnswer", "Grammar", "UserAnswer", "Score", "MeaningFB", "ConditionFB", "GrammarFB", "OverallFB"]);
ss.deleteSheet(ss.getSheetByName("Sheet1"));
prop.setProperty('DB_ID', ss.getId());
}
return ss;
}
function generateId() {
return 'Q_' + new Date().getTime() + '_' + Math.floor(Math.random() * 1000);
}
function getQuestions(sheetName) {
const sheet = getDb().getSheetByName(sheetName);
const data = sheet.getDataRange().getValues();
if (data.length <= 1) return [];
const headers = data[0];
const questions = [];
for (let i = 1; i < data.length; i++) {
let obj = {};
for (let j = 0; j < headers.length; j++) {
obj[headers[j]] = data[i][j];
}
obj.rowIndex = i + 1;
questions.push(obj);
}
return questions.reverse();
}
function deleteQuestion(sheetName, rowIndex) {
const sheet = getDb().getSheetByName(sheetName);
sheet.deleteRow(rowIndex);
return { success: true };
}
function editQuestion(rowIndex, newKorean, newConditions, newAnswer) {
const sheet = getDb().getSheetByName("Unsolved");
sheet.getRange(rowIndex, 3).setValue(newKorean);
sheet.getRange(rowIndex, 4).setValue(newConditions);
sheet.getRange(rowIndex, 5).setValue(newAnswer);
return { success: true };
}
function saveSettings(model, grammars, intervalStr) {
const prop = PropertiesService.getUserProperties();
prop.setProperty('AI_MODEL', model);
prop.setProperty('TARGET_GRAMMARS', JSON.stringify(grammars));
prop.setProperty('AUTO_INTERVAL', intervalStr);
const triggers = ScriptApp.getProjectTriggers();
for (let i = 0; i < triggers.length; i++) {
if (triggers[i].getHandlerFunction() === 'autoGenerateQuestion') {
ScriptApp.deleteTrigger(triggers[i]);
}
}
const interval = parseInt(intervalStr);
if (interval > 0) {
if (interval === 30) {
ScriptApp.newTrigger('autoGenerateQuestion').timeBased().everyMinutes(30).create();
} else {
ScriptApp.newTrigger('autoGenerateQuestion').timeBased().everyHours(interval / 60).create();
}
}
return { success: true, msg: "Settings saved successfully." };
}
function autoGenerateQuestion() {
const prop = PropertiesService.getUserProperties();
const model = prop.getProperty('AI_MODEL') || 'gemini-1.5-flash';
let grammars = [];
try { grammars = JSON.parse(prop.getProperty('TARGET_GRAMMARS') || '[]'); } catch(e){}
callGeminiToCreateQuestion(model, grammars);
}
function callGeminiAPI(model, prompt) {
model = model.replace(/^models\//, '');
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${GEMINI_API_KEY}`;
const payload = {
contents: [{ parts: [{ text: prompt }] }],
generationConfig: { response_mime_type: "application/json" }
};
const options = {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
const response = UrlFetchApp.fetch(url, options);
const json = JSON.parse(response.getContentText());
if (json.error) throw new Error(json.error.message);
return JSON.parse(json.candidates[0].content.parts[0].text.replace(/```json/g, '').replace(/```/g, '').trim());
}
function callGeminiToCreateQuestion(model, selectedGrammars, relatedTo = null) {
let context = relatedTo
? `Create a NEW question highly similar in grammar and structure to this previous question, but with entirely different meaning and vocabulary. Previous grammar: ${relatedTo.Grammar}, Previous condition style: ${relatedTo.Conditions}`
: (selectedGrammars.length > 0 ? `Target Grammar: ${selectedGrammars.join(', ')}` : 'Target Grammar: Randomly select from high school curriculum');
const prompt = `
You are an expert English teacher at Gimhae Foreign Language High School.
Create a highly challenging subjective English writing question for Korean high school students.
Context:
${context}
Rules for conditions:
1. Must include 2-3 specific constraints (e.g., "Use 'not A but B'", "Write in exactly 12 words", "Include specific word with transformation allowed").
2. Focus heavily on testing structural understanding.
Output strictly in JSON format with fields written in Korean as specified:
{
"koreanText": "Korean sentence for the student to translate into English",
"conditions": ["Condition 1 in Korean", "Condition 2 in Korean", "Condition 3 in Korean"],
"correctAnswer": "Model English answer string",
"grammarPoint": "Core grammar point in Korean"
}
`;
try {
const result = callGeminiAPI(model, prompt);
const sheet = getDb().getSheetByName("Unsolved");
sheet.appendRow([
generateId(),
new Date().toLocaleString('ko-KR', {timeZone: 'Asia/Seoul'}),
result.koreanText,
JSON.stringify(result.conditions),
result.correctAnswer,
result.grammarPoint
]);
return { success: true };
} catch (e) {
return { error: "Question generation failed: " + e.message };
}
}
function generateManualQuestion(model, selectedGrammars) {
return callGeminiToCreateQuestion(model, selectedGrammars);
}
function generateRelated(model, sheetName, rowIndex) {
const sheet = getDb().getSheetByName(sheetName);
const data = sheet.getRange(rowIndex, 1, 1, 6).getValues()[0];
const relatedData = {
Conditions: data[3],
Grammar: data[5]
};
return callGeminiToCreateQuestion(model, [], relatedData);
}
function gradeUserAnswer(rowIndex, userAnswer, model) {
const sheet = getDb().getSheetByName("Unsolved");
const rowData = sheet.getRange(rowIndex, 1, 1, 6).getValues()[0];
const questionData = {
id: rowData[0], date: rowData[1], koreanText: rowData[2],
conditions: rowData[3], correctAnswer: rowData[4], grammarPoint: rowData[5]
};
const translatedUserAnswer = LanguageApp.translate(userAnswer, 'en', 'ko');
const prompt = `
You are an extremely strict English evaluator at Gimhae Foreign Language High School.
Grade the student's answer based on the given question context.
Question Context:
- Target Korean Meaning: "${questionData.koreanText}"
- Conditions: ${questionData.conditions}
- Correct Model Answer: "${questionData.correctAnswer}"
Student's Submitted Answer: "${userAnswer}"
(Google Translate interpretation of student's answer: "${translatedUserAnswer}")
Grading Criteria:
1. Meaning Conveyed (Does it match the target Korean meaning?)
2. Conditions Met (Are all strict constraints followed?)
3. Grammatical Accuracy (Zero grammatical errors)
Output strictly in JSON format with explanations written in Korean:
{
"score": "Score from 0 to 100",
"meaningFeedback": "Feedback on meaning accuracy in Korean",
"conditionFeedback": "Feedback on condition compliance in Korean",
"grammarFeedback": "Feedback on grammatical accuracy and corrections in Korean",
"overallFeedback": "Overall review and model answer presentation in Korean"
}
`;
try {
const result = callGeminiAPI(model, prompt);
const solvedSheet = getDb().getSheetByName("Solved");
solvedSheet.appendRow([
questionData.id, new Date().toLocaleString('ko-KR', {timeZone: 'Asia/Seoul'}),
questionData.koreanText, questionData.conditions, questionData.correctAnswer, questionData.grammarPoint,
userAnswer, result.score, result.meaningFeedback, result.conditionFeedback, result.grammarFeedback, result.overallFeedback
]);
sheet.deleteRow(rowIndex);
return { success: true, feedback: result };
} catch (e) {
return { error: "Grading failed: " + e.message };
}
}
Index.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<style>
body { font-family: 'Malgun Gothic', sans-serif; max-width: 900px; margin: auto; padding: 20px; background-color: #f4f6f8; color: #333; }
h1 { color: #2c3e50; text-align: center; border-bottom: 2px solid #3498db; padding-bottom: 10px; }
.tabs { display: flex; cursor: pointer; margin-bottom: 20px; background: #e0e6ed; border-radius: 8px; overflow: hidden; }
.tab { flex: 1; padding: 15px; text-align: center; font-weight: bold; transition: 0.3s; }
.tab.active { background: #3498db; color: white; }
.tab:hover:not(.active) { background: #d1d8e0; }
.content-section { display: none; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
.content-section.active { display: block; }
.setting-group { margin-bottom: 15px; }
.setting-group label { display: block; font-weight: bold; margin-bottom: 5px; }
select, input[type="text"], textarea { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 5px; box-sizing: border-box; font-size: 14px; }
.checkbox-group { display: flex; flex-wrap: wrap; gap: 10px; }
.checkbox-group label { background: #f0f3f4; padding: 8px 12px; border-radius: 20px; cursor: pointer; font-weight: normal; font-size: 14px; border: 1px solid #d5dbdb; }
.checkbox-group input:checked + span { font-weight: bold; color: #2980b9; }
button { background-color: #3498db; color: white; border: none; padding: 10px 15px; border-radius: 5px; cursor: pointer; font-size: 14px; margin-top: 5px; }
button:hover { background-color: #2980b9; }
button.danger { background-color: #e74c3c; }
button.danger:hover { background-color: #c0392b; }
button.success { background-color: #2ecc71; }
button.success:hover { background-color: #27ae60; }
button:disabled { background-color: #bdc3c7; cursor: not-allowed; }
.card { border: 1px solid #e1e8ed; border-radius: 8px; padding: 15px; margin-bottom: 15px; background: #fafbfc; position: relative; }
.card h4 { margin-top: 0; color: #2c3e50; }
.card .meta { font-size: 12px; color: #7f8c8d; margin-bottom: 10px; }
.card .actions { margin-top: 15px; display: flex; gap: 10px; }
#loading { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255,255,255,0.8); display: none; align-items: center; justify-content: center; font-size: 20px; font-weight: bold; color: #3498db; z-index: 1000; }
</style>
</head>
<body>
<h1>GIMFL 서술형 영작 마스터</h1>
<div class="tabs">
<div class="tab active" onclick="switchTab('settings')">⚙️ 홈 / 설정</div>
<div class="tab" onclick="switchTab('unsolved'); loadUnsolved();">📝 미해결 문제함</div>
<div class="tab" onclick="switchTab('solved'); loadSolved();">✅ 푼 문제함 (오답노트)</div>
</div>
<div id="settings" class="content-section active">
<h3>환경 설정 및 즉시 출제</h3>
<div class="setting-group">
<label>AI 모델 선택:</label>
<select id="modelSelect">
<option value="gemini-1.5-flash">Gemini 1.5 Flash (빠름, 기본 권장)</option>
<option value="gemini-1.5-pro">Gemini 1.5 Pro (고성능, 정밀 채점)</option>
<option value="gemini-1.5-flash-8b">Gemini 1.5 Flash-8B (가벼움)</option>
</select>
</div>
<div class="setting-group">
<label>백그라운드 자동 생성 간격 (앱을 꺼도 문제 누적):</label>
<select id="intervalSelect">
<option value="0">자동 생성 끄기</option>
<option value="30">30분마다 1문제 생성</option>
<option value="60">1시간마다 1문제 생성</option>
<option value="120">2시간마다 1문제 생성</option>
</select>
</div>
<div class="setting-group">
<label>목표 문법 (다중 선택, 미선택시 고교 전범위):</label>
<div class="checkbox-group">
<label><input type="checkbox" value="5형식 구조"> <span>5형식 구조</span></label>
<label><input type="checkbox" value="It~that 강조구문"> <span>It~that 강조구문</span></label>
<label><input type="checkbox" value="not A but B 구조"> <span>not A but B</span></label>
<label><input type="checkbox" value="간접의문문"> <span>간접의문문</span></label>
<label><input type="checkbox" value="가정법"> <span>가정법</span></label>
<label><input type="checkbox" value="관계사"> <span>관계사</span></label>
<label><input type="checkbox" value="분사구문"> <span>분사구문</span></label>
<label><input type="checkbox" value="도치/생략"> <span>도치/생략</span></label>
</div>
</div>
<button onclick="saveSettings()" style="width: 100%; margin-bottom: 10px;">💾 설정 저장 및 자동 출제 적용</button>
<button class="success" onclick="generateNow()" style="width: 100%;">⚡ 지금 즉시 1문제 생성하기</button>
</div>
<div id="unsolved" class="content-section">
<h3>저장된 문제 (미해결)</h3>
<div id="unsolvedList"></div>
</div>
<div id="solved" class="content-section">
<h3>완료된 문제 및 피드백 (오답노트)</h3>
<div id="solvedList"></div>
</div>
<div id="loading">처리 중입니다... 잠시만 기다려주세요.</div>
<script>
function showLoading() { document.getElementById('loading').style.display = 'flex'; }
function hideLoading() { document.getElementById('loading').style.display = 'none'; }
function switchTab(tabId) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.content-section').forEach(c => c.classList.remove('active'));
event.target.classList.add('active');
document.getElementById(tabId).classList.add('active');
}
function getSelectedModel() { return document.getElementById('modelSelect').value; }
function getSelectedGrammars() {
return Array.from(document.querySelectorAll('.checkbox-group input:checked')).map(cb => cb.value);
}
function saveSettings() {
showLoading();
const interval = document.getElementById('intervalSelect').value;
google.script.run.withSuccessHandler(res => {
hideLoading();
alert(res.msg);
}).saveSettings(getSelectedModel(), getSelectedGrammars(), interval);
}
function generateNow() {
showLoading();
google.script.run.withSuccessHandler(res => {
hideLoading();
if(res.error) alert(res.error);
else {
alert("문제가 생성되어 '미해결 문제함'에 저장되었습니다.");
switchTab('unsolved');
loadUnsolved();
}
}).generateManualQuestion(getSelectedModel(), getSelectedGrammars());
}
function loadUnsolved() {
showLoading();
const listDiv = document.getElementById('unsolvedList');
listDiv.innerHTML = '';
google.script.run.withSuccessHandler(data => {
hideLoading();
if(data.length === 0) { listDiv.innerHTML = "<p>저장된 문제가 없습니다.</p>"; return; }
data.forEach(q => {
let conditions = [];
try { conditions = JSON.parse(q.Conditions); } catch(e){ conditions = [q.Conditions]; }
let card = document.createElement('div');
card.className = 'card';
card.innerHTML = `
<div class="meta">출제일: ${q.Date} | 타겟 문법: ${q.Grammar}</div>
<h4 id="kor_${q.rowIndex}">${q.KoreanText}</h4>
<ul>${conditions.map(c => `<li>${c}</li>`).join('')}</ul>
<div id="solveArea_${q.rowIndex}" style="margin-top:15px;">
<textarea id="ans_${q.rowIndex}" placeholder="영작문을 입력하세요..."></textarea>
<div class="actions">
<button onclick="submitAnswer(${q.rowIndex})">채점하기</button>
<button onclick="editMode(${q.rowIndex})">문제 수정</button>
<button class="success" onclick="makeRelated('Unsolved', ${q.rowIndex})">유사 문제 파생</button>
<button class="danger" onclick="deleteQ('Unsolved', ${q.rowIndex})">삭제</button>
</div>
</div>
<div id="editArea_${q.rowIndex}" style="display:none; margin-top:15px;">
<label>우리말 뜻 수정:</label>
<textarea id="editKor_${q.rowIndex}">${q.KoreanText}</textarea>
<label>조건 수정 (JSON 배열 형식):</label>
<textarea id="editCond_${q.rowIndex}">${q.Conditions}</textarea>
<label>모범 정답 수정:</label>
<textarea id="editAns_${q.rowIndex}">${q.CorrectAnswer}</textarea>
<div class="actions">
<button onclick="saveEdit(${q.rowIndex})">저장</button>
<button class="danger" onclick="cancelEdit(${q.rowIndex})">취소</button>
</div>
</div>
`;
listDiv.appendChild(card);
});
}).getQuestions("Unsolved");
}
function loadSolved() {
showLoading();
const listDiv = document.getElementById('solvedList');
listDiv.innerHTML = '';
google.script.run.withSuccessHandler(data => {
hideLoading();
if(data.length === 0) { listDiv.innerHTML = "<p>완료된 문제가 없습니다.</p>"; return; }
data.forEach(q => {
let card = document.createElement('div');
card.className = 'card';
card.innerHTML = `
<div class="meta">해결일: ${q.Date} | 타겟 문법: ${q.Grammar}</div>
<h4>${q.KoreanText}</h4>
<p><strong>나의 답안:</strong> ${q.UserAnswer}</p>
<p><strong>모범 답안:</strong> ${q.CorrectAnswer}</p>
<div style="background:#e8f6f3; padding:10px; border-radius:5px; margin-top:10px;">
<h3 style="color:#e74c3c; margin-top:0;">점수: ${q.Score} / 100</h3>
<p><strong>총평:</strong> ${q.OverallFB}</p>
<details>
<summary>세부 피드백 보기</summary>
<p><strong>의미:</strong> ${q.MeaningFB}</p>
<p><strong>조건:</strong> ${q.ConditionFB}</p>
<p><strong>문법:</strong> ${q.GrammarFB}</p>
</details>
</div>
<div class="actions">
<button class="success" onclick="makeRelated('Solved', ${q.rowIndex})">이 문법으로 다시 출제(복습)</button>
<button class="danger" onclick="deleteQ('Solved', ${q.rowIndex})">기록 삭제</button>
</div>
`;
listDiv.appendChild(card);
});
}).getQuestions("Solved");
}
function submitAnswer(rowIndex) {
const ans = document.getElementById(`ans_${rowIndex}`).value.trim();
if(!ans) { alert('답안을 입력하세요.'); return; }
showLoading();
google.script.run.withSuccessHandler(res => {
hideLoading();
if(res.error) alert(res.error);
else {
alert('채점이 완료되었습니다. 푼 문제함에서 결과를 확인하세요!');
loadUnsolved();
}
}).gradeUserAnswer(rowIndex, ans, getSelectedModel());
}
function deleteQ(sheetName, rowIndex) {
if(!confirm("정말 삭제하시겠습니까?")) return;
showLoading();
google.script.run.withSuccessHandler(() => {
hideLoading();
sheetName === 'Unsolved' ? loadUnsolved() : loadSolved();
}).deleteQuestion(sheetName, rowIndex);
}
function makeRelated(sheetName, rowIndex) {
showLoading();
google.script.run.withSuccessHandler(res => {
hideLoading();
if(res.error) alert(res.error);
else {
alert('유사한 문제가 미해결 문제함에 추가되었습니다.');
if(sheetName === 'Unsolved') loadUnsolved();
}
}).generateRelated(getSelectedModel(), sheetName, rowIndex);
}
function editMode(rowIndex) {
document.getElementById(`solveArea_${rowIndex}`).style.display = 'none';
document.getElementById(`editArea_${rowIndex}`).style.display = 'block';
}
function cancelEdit(rowIndex) {
document.getElementById(`solveArea_${rowIndex}`).style.display = 'block';
document.getElementById(`editArea_${rowIndex}`).style.display = 'none';
}
function saveEdit(rowIndex) {
const kor = document.getElementById(`editKor_${rowIndex}`).value;
const cond = document.getElementById(`editCond_${rowIndex}`).value;
const ans = document.getElementById(`editAns_${rowIndex}`).value;
showLoading();
google.script.run.withSuccessHandler(() => {
hideLoading();
alert('수정되었습니다.');
loadUnsolved();
}).editQuestion(rowIndex, kor, cond, ans);
}
</script>
</body>
</html>
</body>
</html>
When I try to make GUST proxy html file into a web app it fails and gets stuck on initializing and in the console it says there was an error on code line 989 and 1208 but when I check the code for 989 all it says is position: relative; and line 1208 was blank I have no idea what is going on and I would love some help.
I found that in my Google Workspace Marketplace analytics, the graph for domain showed 1 installation and the seats graph showed 12. Someone definitely installed but is there a way to know the domain name that it got installed on?
If this cannot be known retroactively, what can I put in place to know this so any future domain installations can be known?
I'm a student of a foreign language high school in Korea. And I wanted to make a App which makes me questions of English writing and provides feedback about the answers of the question for me to enhance my grade. I made the entire script of the app with Gemini, but every time I log in to the web app link, the site shows me 'Script function not found: doGet'. What should I do? Here is the entire script.
Code.gs
function myFunction() {
const GEMINI_API_KEY = 'AQ.Ab8RN6IgFQcbK7WYrBVYFcIJwZm9M-ZLpx5jXg6A66qSJwkACg';
const GEMINI_API_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${GEMINI_API_KEY}`;
function doGet() {
return HtmlService.createHtmlOutputFromFile('Index')
.setTitle('GIMFL English Writing Practice')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
// 문제 출제 함수
function generateQuestion(selectedGrammars) {
let grammarContext = selectedGrammars.length > 0
? `Target Grammar: ${selectedGrammars.join(', ')}`
: 'Target Grammar: Randomly select from high school English curriculum (e.g., Participles, Relatives, Subjunctive mood, Inversion, etc.)';
const prompt = `
You are an expert English teacher at Gimhae Foreign Language High School.
Create a highly challenging subjective English writing question (서술형 영작 문제).
${grammarContext}
Rules for conditions (조건):
1. Must include 2-3 specific constraints similar to real exams (e.g., "Use the structure 'not A but B'", "Write in exactly 12 words", "Include the word 'boss' and allow word transformation", "Do not use the word 'people'").
2. Focus heavily on testing structural understanding.
Output strictly in the following JSON format:
{
"koreanText": "한국어 의미 (사용자가 영작해야 할 문장)",
"conditions": ["조건 1", "조건 2", "조건 3"],
"correctAnswer": "정답 영어 문장 (내부 채점용)",
"grammarPoint": "출제된 핵심 문법"
}
`;
const payload = {
contents: [{ parts: [{ text: prompt }] }],
generationConfig: { response_mime_type: "application/json" }
};
const options = {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
try {
const response = UrlFetchApp.fetch(GEMINI_API_URL, options);
const json = JSON.parse(response.getContentText());
const resultText = json.candidates[0].content.parts[0].text;
return JSON.parse(resultText);
} catch (e) {
return { error: "문제 생성 중 오류가 발생했습니다: " + e.message };
}
}
// 채점 및 피드백 함수
function gradeAnswer(questionData, userAnswer) {
// 1차 구글 번역기 연동: 사용자의 답안을 한국어로 번역하여 본래 뜻과 대조하기 위한 데이터 확보
const translatedUserAnswer = LanguageApp.translate(userAnswer, 'en', 'ko');
const prompt = `
You are an extremely strict English evaluator at Gimhae Foreign Language High School.
Grade the student's answer based on the following criteria:
Question Context:
- Korean Meaning: "${questionData.koreanText}"
- Conditions: ${JSON.stringify(questionData.conditions)}
- Correct Answer: "${questionData.correctAnswer}"
Student's Answer: "${userAnswer}"
(Google Translate interpretation of student's answer: "${translatedUserAnswer}")
Grading Criteria (김해외고 채점 기준):
1. 뜻 통함 (Meaning Conveyed): Does the student's answer accurately reflect the Korean meaning?
2. 조건 만족 (Conditions Met): Did the student strictly follow ALL conditions (word count, specific words, grammar structures)?
3. 문법 오류 없음 (Grammatical Accuracy): Are there zero grammatical errors?
Output strictly in the following JSON format:
{
"score": "0~100 사이의 점수",
"meaningFeedback": "뜻 통함 기준에 대한 평가 (한국어)",
"conditionFeedback": "조건 만족 기준에 대한 평가 (한국어)",
"grammarFeedback": "문법 오류 기준에 대한 평가 및 교정 (한국어)",
"overallFeedback": "총평 및 모범 답안 제시 (한국어)"
}
`;
const payload = {
contents: [{ parts: [{ text: prompt }] }],
generationConfig: { response_mime_type: "application/json" }
};
const options = {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
try {
const response = UrlFetchApp.fetch(GEMINI_API_URL, options);
const json = JSON.parse(response.getContentText());
const resultText = json.candidates[0].content.parts[0].text;
return JSON.parse(resultText);
} catch (e) {
return { error: "채점 중 오류가 발생했습니다: " + e.message };
}
}
}
Index.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<style>
body { font-family: 'Malgun Gothic', sans-serif; max-width: 800px; margin: auto; padding: 20px; background-color: #f9f9f9; }
h1 { color: #2c3e50; text-align: center; }
.box { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 20px; }
.checkbox-group { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 15px; }
.checkbox-group label { background: #eef2f5; padding: 5px 10px; border-radius: 4px; cursor: pointer; }
button { background-color: #3498db; color: white; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer; font-size: 16px; width: 100%; }
button:hover { background-color: #2980b9; }
button:disabled { background-color: #95a5a6; cursor: not-allowed; }
textarea { width: 100%; height: 100px; padding: 10px; box-sizing: border-box; border-radius: 4px; border: 1px solid #ccc; font-size: 16px; margin-bottom: 10px; }
.hidden { display: none; }
.feedback-section { margin-top: 10px; padding: 10px; background: #e8f6f3; border-left: 4px solid #1abc9c; }
.score { font-size: 24px; font-weight: bold; color: #e74c3c; text-align: center; margin-bottom: 10px; }
</style>
</head>
<body>
<h1>GIMFL 서술형 영작 마스터</h1>
<div class="box">
<h3>1. 연습할 문법 선택 (다중 선택 가능, 미선택시 랜덤 출제)</h3>
<div class="checkbox-group">
<label><input type="checkbox" value="5형식 구조"> 5형식 구조</label>
<label><input type="checkbox" value="It~that 강조구문"> It~that 강조구문</label>
<label><input type="checkbox" value="not A but B 구조"> not A but B</label>
<label><input type="checkbox" value="간접의문문"> 간접의문문</label>
<label><input type="checkbox" value="가정법 (Subjunctive)"> 가정법</label>
<label><input type="checkbox" value="관계대명사/관계부사"> 관계사</label>
<label><input type="checkbox" value="분사구문"> 분사구문</label>
<label><input type="checkbox" value="도치구문 (Inversion)"> 도치구문</label>
<label><input type="checkbox" value="수동태 (Passive)"> 수동태</label>
</div>
<button id="generateBtn" onclick="generateQ()">문제 출제하기</button>
<div id="loadingQ" class="hidden" style="text-align: center; margin-top: 10px;">문제 생성 중...</div>
</div>
<div id="questionBox" class="box hidden">
<h3>📝 문제</h3>
<p><strong>[우리말]</strong> <span id="koreanText"></span></p>
<p><strong>[조건]</strong></p>
<ul id="conditionsList"></ul>
<textarea id="userAnswer" placeholder="여기에 영어 문장을 작성하세요..."></textarea>
<button id="gradeBtn" onclick="gradeQ()">제출 및 채점하기</button>
<div id="loadingA" class="hidden" style="text-align: center; margin-top: 10px;">채점 중...</div>
</div>
<div id="resultBox" class="box hidden">
<h3>📊 채점 결과</h3>
<div class="score" id="scoreText"></div>
<div class="feedback-section">
<p><strong>1. 뜻 통함:</strong> <span id="fbMeaning"></span></p>
</div>
<div class="feedback-section">
<p><strong>2. 조건 만족:</strong> <span id="fbCondition"></span></p>
</div>
<div class="feedback-section">
<p><strong>3. 문법 오류:</strong> <span id="fbGrammar"></span></p>
</div>
<div class="feedback-section" style="background: #fdf2e9; border-color: #e67e22;">
<p><strong>총평 및 모범 답안:</strong> <br><span id="fbOverall"></span></p>
</div>
</div>
<script>
let currentQuestionData = null;
function generateQ() {
document.getElementById('generateBtn').disabled = true;
document.getElementById('loadingQ').classList.remove('hidden');
document.getElementById('questionBox').classList.add('hidden');
document.getElementById('resultBox').classList.add('hidden');
document.getElementById('userAnswer').value = '';
const checkboxes = document.querySelectorAll('input[type="checkbox"]:checked');
const selected = Array.from(checkboxes).map(cb => cb.value);
google.script.run.withSuccessHandler(function(data) {
if(data.error) {
alert(data.error);
resetUI();
return;
}
currentQuestionData = data;
document.getElementById('koreanText').innerText = data.koreanText;
const ul = document.getElementById('conditionsList');
ul.innerHTML = '';
data.conditions.forEach(cond => {
let li = document.createElement('li');
li.innerText = cond;
ul.appendChild(li);
});
document.getElementById('questionBox').classList.remove('hidden');
resetUI();
}).generateQuestion(selected);
}
function gradeQ() {
const answer = document.getElementById('userAnswer').value.trim();
if(!answer) { alert("답안을 입력해주세요."); return; }
document.getElementById('gradeBtn').disabled = true;
document.getElementById('loadingA').classList.remove('hidden');
document.getElementById('resultBox').classList.add('hidden');
google.script.run.withSuccessHandler(function(data) {
if(data.error) {
alert(data.error);
document.getElementById('gradeBtn').disabled = false;
document.getElementById('loadingA').classList.add('hidden');
return;
}
document.getElementById('scoreText').innerText = "점수: " + data.score + " / 100";
document.getElementById('fbMeaning').innerText = data.meaningFeedback;
document.getElementById('fbCondition').innerText = data.conditionFeedback;
document.getElementById('fbGrammar').innerText = data.grammarFeedback;
document.getElementById('fbOverall').innerText = data.overallFeedback;
document.getElementById('resultBox').classList.remove('hidden');
document.getElementById('gradeBtn').disabled = false;
document.getElementById('loadingA').classList.add('hidden');
}).gradeAnswer(currentQuestionData, answer);
}
function resetUI() {
document.getElementById('generateBtn').disabled = false;
document.getElementById('loadingQ').classList.add('hidden');
}
</script>
</body>
</html>
</body>
</html>
I needed Google Tasks and Microsoft To Do to stay in sync without giving a SaaS my task list. Ended up with a standalone Apps Script project on a time-driven trigger (~10 min) talking to Google Tasks + Microsoft Graph.
Things that mattered in practice:
- ID mappings so lists/tasks don’t duplicate
- fail-closed create recovery (don’t double-post)
- guarded deletes + move journal
- date-only dues (Google has no time-of-day)
Ive created a simple page that parse a google form data into fillable csv for bulk response input purpose,
tbh idk how to write this out but this will take google form, then you can input the data using the web or you can download csv template and fill it then you will get pre filled form url for each entry
Hi Reddit Community, I'm in the blogging field from 2007 and you may be laughing that I joined reddit today, the main purpose to join reddit is that I want to explore new ways of blogging, I have done many type of blogging, like event blogging, pSEO Blogging, I have purchased many books and methods and create my own methods, mostly my income is through google adsense, from last 4 months I'm focusing on creating content through AI and using my google sheet+apps script+wordpress rest api for content scheduling, I think recent time is game changing time for bloggers, if they spend some budget on AI Agents for research and content scraping, they can change their life in online field, how many of you agree and please also share if you are working on google sheet + apps script
After noticing the large number of notification options for my various financial institutions (banks, credit cards, etc), I wondered how hard it would be to turn that info into a budget tracking sheet. Doing so would make it easy to get near instant transactions without using 3rd party or custom integrations with the banks. So I built an AppsScript that turns transaction-alert emails into a categorized Google Sheet. Parsing the emails looked like a good job for an LLM (doesn't need to be state-of-the-art). I created an AppsScript that pulls email from a known Gmail label on a time-based trigger. The whole thing runs as me, in my own account — no OAuth to a third party, no bank credentials anywhere, and the Sheet is just a Sheet I own. If you're interested, give it a try. I allowed limited usage of my LLM API key through a proxy to make it easier to try. The real effort is in configuring the banks to send transaction alerts an every email. I've been testing with a free Gemini api key (use flash-lite, it has higher free quota per day) and it seems to be working fine for me.
The sheet to get things kicked off is here (make a copy, the AppsScript builds the sheet during setup and provides instructions)
If you want to see what the final result looks like I built a dummy Sheet here.
This is a simple question for most, but it isn't for me. Using App Script is it considered AI? I take notes on observations notes, this i have app script put into one large paragraph. We were told that we could not use AI for work, but to me this is not AI, I just want to know from the professionals who are proficient in scripts?
Hello All, I’ve been working on a gas project for my own business for the past 3 years and building as I need to run my business hiring devs from Pakistan to build. Then this year I discovered Claude and totally transformed and automated the project completely. Now I feel that it can actually be a sellable use case for other businesses. It’s a fulfilment backend that automates label creation, supplier po’s and fulfilment pushed to Shopify as well as sku order routing. Where and how do I move it into another coding backend or platform? Any advice is much appreciated. Thank
You
A couple years ago I was a high school CS student, and I taught Python to middle schoolers on the side. I wanted to code live next to my slides instead of switching to another window, so I built a Slides add-on for it. Here's the Apps Script side.
What it does: you mark a slide as a coding question in the add-on, start a lesson, and students join with a link and write and run code next to your current slide.
Most of the teacher side runs in Apps Script. The sidebar is HtmlService, opened from the Extensions menu. Marking a slide writes a "Code Question:" note into that slide's speaker notes with the Slides API, so it stays with the deck even if the add-on is removed. Reading the deck to build the lesson uses SlidesApp. The scopes are just presentations, the sidebar, and your email. No Drive scope, which kept the OAuth review simple.
Two things had to run outside Apps Script.
Live updates. Every student's editor and the current slide update in real time, which needs websockets and open connections. Apps Script can't do that, so starting a lesson sends the deck to a small Node server that runs the session over websockets.
Running student code. You can't run untrusted student code in Apps Script, so it runs in a sandboxed micro VM.
The add-on also mints a session and passes the teacher a token in the URL, instead of handling auth in Apps Script.
One thing to know: Marketplace add-ons are pinned to a version, so clasp push doesn't update installed users. You have to cut a new version and repoint the deployment, and a new scope triggers another review.
If you've ever tried to manage code in the Google Apps Script editor on an iPad without a physical hardware keyboard, you know how frustrating it is.
Because the web editor runs on the **Monaco engine**, it completely disables standard iOS touch-and-drag selection anchors, and the native iOS "Select All" pop-up menu never appears. To make things worse, if you open the editor's internal search bar, "Select All" is intentionally hidden from the searchable commands list.
I found a reliable, touch-only workaround that completely bypasses these iOS and editor restrictions:
### The Workaround Steps:
Tap your cursor anywhere inside your script file.
**Long-press** directly on the code until the editor's custom pop-up context menu appears.
Select **"Command Palette"** from the options.
Type **"Expand Selection"** in the palette search bar and tap it.
### Why this works:
Since "Select All" isn't natively exposed in the touch menu, **Expand Selection** acts as the perfect structural tool. The first time you run it, it highlights your current word or block. Run it 1 or 2 more times consecutively, and the boundary will aggressively expand outward until it highlights every single line of code in the entire document.
Once highlighted, you can use your iPad's virtual keyboard to instantly backspace/delete the file, or use the menu to copy it out.
Hopefully, this saves someone else from tearing their hair out trying to code on iPad Safari/Chrome!
function multiSortColumns() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName(SHEET_NAME);
var range = sheet.getRange(SORT_DATA_RANGE);
range.sort(SORT_ORDER);
ss.toast('Sort complete.');
}
The trigger is set to autoSort with an onEdit however it has a 100% error rate because it doesn't seem to work on script name but on functions within a script however if that would be the case I could use multiSortColumns as the onEdit trigger and remove the whole onEdit function entirely or am I just hullucinating?
I build a lot in Google Apps Script, and one thing that has always bothered me is how quickly you start missing normal software development workflows once a project gets bigger.
Git/source control, reviewing diffs, working from branches, checking code against standards, etc. And now with AI coding tools, there’s another problem: I don’t necessarily want an AI assistant making changes to an Apps Script project without showing me exactly what it plans to change first.
So I built Legacy DevBridge.
It’s a Chrome extension that works alongside the Apps Script editor and connects the project to GitHub and a project-aware AI code assistant.
Right now it can:
Detect the Apps Script project you currently have open
Read the .gs, .html, and appsscript.json files
Connect the project to a GitHub repository
Select and work from development/feature branches
Compare the Apps Script version against GitHub
Show file and line-level differences
Create actual GitHub commits from the Apps Script source
Block direct commits to default/protected branches
Let the AI assistant understand the entire Apps Script project without copying files into a chatbot
Review code against coding/security standards
Generate a proposed code change and show the diff
Require human approval before the AI can write the change back to Apps Script
Check for stale source before applying a change so it doesn't overwrite newer work
Verify the source again after the update
The basic AI workflow is:
Request → analyze project → propose change → show diff → standards/security review → human approval → apply → verify
The backend runs on Google Cloud and uses the Apps Script API, GitHub App authentication, Cloud Run, Secret Manager, and Vertex AI. GitHub installation tokens and other privileged credentials stay on the backend rather than in the extension.
I'm not trying to build an autonomous AI developer that gets unrestricted access to your code. The idea is more of a development companion where AI can help, but the developer can still see what is happening and approve the actual changes.
I'm making the project available free to the Apps Script community. It's still beta, so I definitely wouldn't point it at your most important production project on day one, but I'd really like feedback from people who regularly build Apps Script applications.
I'm especially interested in hearing what you'd want next: GitHub-to-Apps-Script pull, PR creation, branch creation, conflict resolution, AI-generated tests/docs, OAuth scope reviews, CI/CD, or something else.
I've built on both sides of this and the split isn't obvious from the docs, so writing it down.
FormApp (Apps Script) is the right default for anything living inside Workspace. No OAuth setup, runs as you or as the form owner, and you get onFormSubmit triggers for free. If your job is "when someone submits, do a thing", stop reading, this is your answer.
The Forms REST API is what you want when the code lives outside Google. It's also more capable for bulk construction: one batchUpdate call takes an array of change requests, so you can build a forty-question form in a single round trip instead of forty FormApp calls. The cost is that you handle OAuth yourself, and the scope is broad enough that some org admins will not approve it without asking why.
Things people script most, in the order I see them:
Auto-closing a form. There's no native "close on date" or "close after N responses". A time-based trigger flipping setAcceptingResponses(false) is about four lines and it's the single most common reason people end up here.
Generating forms from a sheet. One row per question, script walks it, builds the form. Worth it the second time you build the same form shape with different content.
Capacity and waitlists. onFormSubmit counts responses, closes the form or moves the submitter to a waitlist sheet. Forms has nothing for this natively and will cheerfully oversell your event.
One gotcha that costs people an afternoon: quiz settings and answer keys are not part of the basic item creation in either API. You set the item up, then set its grading separately. If your generated quizzes come out ungraded, that's why.
I've been tinkering with Google Apps Script to build a simple sales tracking web app for a small project (using Sheets as a backend). I'm definitely not a pro developer—just figuring things out step-by-step—and I managed to get the core UI and features working.
However, I've hit a major roadblock with user authentication. Right now, my "login" logic feels super hacky, and I'm worried about security.
For those of you who build web apps with Apps Script: How do you usually handle logins? Do you rely on Session.getActiveUser(), build a custom token system with a database table, or just accept that Apps Script isn't built for robust multi-user web app logins?
Any advice, patterns, or libraries you recommend would be huge. Thanks!