Module 4 — Send (Apps Script) and the full flow
Office Hours: Session 15 — Thursday, 17 December 2026
Session goal: a custom step that takes inputs from earlier steps, restores the links, formats the email, and sends it — then wire the whole flow.
Bring: Modules 1–3 done.
What this step does
- Takes two inputs: the Writer Checker's briefing and
linkMapJsonfrom CoS Gather. - Strips whatever markdown the models produced, then lays out every section the same way: colored RACI label, bold title, link, one-line summary.
- Turns every ref into its real link; flags any ref it can't find instead of making a broken link.
- Removes the Checker notes (they stay visible in Studio's Activity tab) and emails the briefing to whoever runs the flow.
Part 1 — Create the project
- On script.google.com, click New project and rename it CoS Send.
- Project Settings → check "Show 'appsscript.json' manifest file in editor."
- Editor →
Code.gs: select all, delete, paste the Code.gs block below. appsscript.json: select all, delete, paste the manifest block below.- Save. Check for a red error.
- Deploy → Test deployments → Install → Done. Reload the page if Install doesn't show.
Keep it a separate project from CoS Gather: each manifest lists only the step whose code is in that project.
Part 2 — Finish the flow
- Refresh Workspace Studio.
- Add CoS Send after the Writer Checker step.
- In its card, use the variable picker to set Briefing to the Writer Checker's response and Link map to
linkMapJsonfrom CoS Gather. - Click Turn on and run it. The briefing lands in your inbox.
- When it works, switch the starter from Start manually to a weekday-morning schedule.
In the session
- Live debug: "Script function not found" — a manifest listing a step whose code isn't in that project.
- Live debug: titles not bold because the model added bullet markers — why Send normalizes the text before laying it out.
- Stretch: change the label colors or add a footer line to the email.
Code — appsscript.json
{
"timeZone": "America/New_York",
"dependencies": {},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"addOns": {
"common": {
"name": "CoS Send",
"logoUrl": "https://www.gstatic.com/images/branding/productlogos/buganizer/v1/192px.svg",
"useLocaleFromApp": true
},
"studio": {
"flows": {
"workflowElements": [
{
"id": "cosSend",
"state": "ACTIVE",
"name": "CoS Send",
"description": "Turns refs into links and emails the CoS Briefing to you.",
"workflowAction": {
"inputs": [
{ "id": "briefingText", "description": "Briefing text from the Writer Checker step", "cardinality": "SINGLE", "dataType": { "basicType": "STRING" } },
{ "id": "linkMapJson", "description": "Link map from CoS Gather", "cardinality": "SINGLE", "dataType": { "basicType": "STRING" } }
],
"outputs": [
{ "id": "sendStatus", "description": "What was sent and any unknown refs", "cardinality": "SINGLE", "dataType": { "basicType": "STRING" } }
],
"onConfigFunction": "onConfigSend",
"onExecuteFunction": "onExecuteSend"
}
}
]
}
}
}
}
Code — Code.gs
function onConfigSend() {
const vars = () => CardService.newHostAppDataSource()
.setWorkflowDataSource(CardService.newWorkflowDataSource().setIncludeVariables(true));
const section = CardService.newCardSection()
.setHeader('CoS Send')
.addWidget(CardService.newTextInput().setFieldName('briefingText').setTitle('Briefing (Writer Checker output)')
.setHostAppDataSource(vars()))
.addWidget(CardService.newTextInput().setFieldName('linkMapJson').setTitle('Link map (from CoS Gather)')
.setHostAppDataSource(vars()));
return CardService.newCardBuilder().addSection(section).build();
}
function onExecuteSend(event) {
const inputs = (event && event.workflow && event.workflow.actionInvocation && event.workflow.actionInvocation.inputs) || {};
const text = stepInput_(inputs, 'briefingText');
let linkMap = {};
try { linkMap = JSON.parse(stepInput_(inputs, 'linkMapJson') || '{}'); } catch (e) { linkMap = {}; }
const status = sendBriefing(text, linkMap);
const action = AddOnsResponseService.newReturnOutputVariablesAction().setVariableDataMap({
sendStatus: AddOnsResponseService.newVariableData().addStringValue(status),
});
return AddOnsResponseService.newRenderActionBuilder()
.setHostAppAction(AddOnsResponseService.newHostAppAction().setWorkflowAction(action))
.build();
}
function stepInput_(inputs, name) {
const v = inputs[name];
if (!v) return '';
if (v.stringValues && v.stringValues.length) return v.stringValues[0];
if (typeof v.value === 'string') return v.value;
return typeof v === 'string' ? v : '';
}
function sendBriefing(text, linkMap) {
const me = Session.getActiveUser().getEmail(); // whoever runs the flow
const tz = CalendarApp.getDefaultCalendar().getTimeZone(); // that person's own time zone
const name = me.split('@')[0].split(/[._-]/).map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
const subject = `${name}'s CoS Briefing – ${Utilities.formatDate(new Date(), tz, 'EEEE, MMMM d, yyyy')}`;
const unknown = [];
// Studio turns domain names into google.com redirect links; strip all links back to plain text
const clean = String(text || '')
.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, '$1')
.replace(/https?:\/\/www\.google\.com\/url\?\S+/g, '')
.replace(/\n\s*Checker notes\b[\s\S]*$/i, ''); // checker notes stay in Studio Activity, not the email
const HEADING = /^(Daily Summary|Upcoming Deliverables|Unmapped Operational Red Flags|Upcoming Calendar|Weekly Retrospective|Closing Notes)\b/i;
const SUBHEAD = /^\[(RESPONSIBLE\/ACCOUNTABLE|CONSULTED\/INFORMED)\]/;
const TAGLINE = /^\[(RESPONSIBLE|ACCOUNTABLE|CONSULTED|INFORMED)\]\s*/;
const TAG_COLOR = { RESPONSIBLE: '#1a73e8', ACCOUNTABLE: '#188038', CONSULTED: '#e37400', INFORMED: '#5f6368' };
const esc = s => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
const fmtInline = s => esc(s).replace(/\[([EMNPRDT]{1,2}\d+)\]/g, (m, ref) => {
if (linkMap[ref]) return `<a href="${linkMap[ref]}" style="color:#1a73e8">[${ref}]</a>`;
unknown.push(ref);
return `[${ref}?]`;
});
// Normalize whatever markdown the models used, then lay out every section the same way:
// section heading > (optional group) > bold item line with a colored tag > plain one-line summary
const html = clean.split('\n').map(raw => {
const line = raw.trim()
.replace(/^#+\s*/, '') // markdown headings
.replace(/^[*\-•]\s+/, '') // bullet markers
.replace(/\*\*|__/g, '') // bold markers
.replace(/^\*(.+)\*$/, '$1') // a fully italic line
.replace(/^Deeper Dive:\s*/i, '')
.trim();
if (!line) return '';
if (SUBHEAD.test(line)) {
return `<div style="margin:14px 0 4px;font-size:13px;font-weight:bold;color:#5f6368;text-transform:uppercase;letter-spacing:.3px">${fmtInline(line.replace(/^\[[^\]]+\]\s*—?\s*/, ''))}</div>`;
}
const tag = line.match(TAGLINE);
if (tag) {
const t = tag[1];
return `<div style="margin:12px 0 2px"><span style="display:inline-block;min-width:96px;font-size:11px;font-weight:bold;color:#fff;background:${TAG_COLOR[t]};border-radius:3px;padding:1px 6px;margin-right:8px;text-align:center">${t}</span><b>${fmtInline(line.replace(TAGLINE, ''))}</b></div>`;
}
if (HEADING.test(line)) {
return `<h3 style="margin:24px 0 6px;padding-bottom:4px;border-bottom:2px solid #dadce0;font-size:16px">${fmtInline(line.replace(/\s*\(.*\)\s*$/, ''))}</h3>`;
}
return `<div style="margin:0 0 4px 112px;color:#3c4043">${fmtInline(line)}</div>`;
}).join('\n');
const note = unknown.length
? `<hr><p style="color:#a00">Unknown refs (no link): ${[...new Set(unknown)].join(', ')}</p>` : '';
GmailApp.sendEmail(me, subject, clean, { htmlBody: `<div style="font-family:Arial,sans-serif;font-size:14px;line-height:1.45;max-width:760px">${html}${note}</div>` });
return `Sent "${subject}" to ${me}. Unknown refs: ${unknown.length ? [...new Set(unknown)].join(', ') : 'none'}`;
}
Changing things later
- Edited code? Just save — test deployments run the latest saved code.
- New permissions needed? Deploy → Test deployments → Uninstall, then Install.
Troubleshooting
| What you see | What to do |
|---|---|
| No Install button under Test deployments | Reload the browser page; check appsscript.json saved without a red error. |
| Studio: "Something went wrong with the authentication…" | Deploy → Test deployments → Uninstall, then Install. Refresh Studio. |
| Studio: "Script function not found" | A manifest lists a step whose code isn't in that project. Re-paste both files for that project from this page. |
| Turn on is greyed out | A step isn't fully set up — look for the step with a warning icon (often a missing variable). |
| Analyst says "No CoS data provided" | The prompt has typed text instead of the gatherJson variable. Re-insert it with the variable picker. |
| A step doesn't appear in Studio | Refresh Studio; confirm the test deployment is installed in that project. |
Build kit home · 1 Gather · 2 Analyst · 3 Writer · 4 Send · 5 Build your own