Part 9 of an ongoing series on vibe coding in EdTech.
A course module arrives as a Word document. A subject matter expert wrote it, and it’s good: objectives at the top, sections under headings, a figure here, a YouTube video there, a reflection activity and a reference list at the end. Then somebody has to rebuild all of it inside Canvas, page by page, in the agency’s branded template with its own icons and heading styles. That work takes care but very little judgment. By hand it runs a few hours per module of copying and reformatting, which across a ten-module course means days spent retyping content that already exists.
This summer I built a tool to take that step away for a course we were developing with a state agency on its Canvas instance. There were two obvious ways to go about it, and I tried both before landing on a third.
The first obvious way was AI, and I did try it. I handed a module document to a powerful model, Claude Opus, and asked it to produce the Canvas package. The results were genuinely good, and it was fast. The catch was the reason the results were good. The model was essentially doing what a program could do: reading the headings, finding curly-brace markers like {insert Figure 1}, and placing each piece in the right spot in the template.
The documents weren’t asking for generation, because they were already written. What they needed was translation from one structure into another, and that structure held steady from module to module, with the same markers, the same heading pattern, and the same closing sections in the same order. Doing that translation with a model meant paying for it in tokens and energy on every run, for this course and for every future course built the same way. A task with a stable pattern is a task for code that follows the pattern. That’s the argument from the previous piece in this thread, taken one step further to a tool with no model in it at all.
The second obvious way was the one I’ve been teaching, so I built it in Google Apps Script. It read a Google Doc, rendered each page through the template, and published the whole module into Canvas over the API, with a build log in a spreadsheet so an interrupted run could resume where it stopped. I finished that version and had its tests passing offline. Before it ever touched a live course, I stopped and set the Canvas half of it aside for good.
What the upload step was costing
The Apps Script version carried an operating cost that I didn’t fully see until I wrote it down. A Canvas API token had to live in a spreadsheet. There was an OAuth consent screen, a web app deployment to maintain, and a Google account requirement for anyone who used it. Adding the connection to Canvas also changed the permissions the script requested, and Apps Script makes everyone using a tool authorize it again whenever those permissions change. None of that was hard for me, but all of it would have become somebody else’s problem after handoff.
When I asked what the agency actually needed, the answer was smaller than what I was building. They needed a file they could import themselves, and Canvas already has a door for that. Under a course’s settings, Import Course Content accepts a Common Cartridge package, the same format Canvas produces when it exports a course. So the tool became a single HTML file that runs in the browser straight from the desktop. You drop in the Word documents, preview every page it will create, and download one package to import. It needs no account and no token, there’s nothing to deploy, and nothing leaves the computer. The whole thing, template icons included, is about 445 KB.
Handing the upload step back to a person turned out to be the decision that removed everything else, and it also put a human exactly where one belongs. Everything the tool creates arrives unpublished. Agency staff review the pages and publish them when they’re ready, so the tool handles the retyping while people still decide what learners see.
I’ve since run every remaining module in the course through it and tested it against other documents, and it has held up well. The output isn’t finished work, and pages still needed some polish by hand in Canvas afterward. But that polish takes far less time than the few hours a module used to take, the pages come out consistent from one module to the next, and none of it required a single call to a model.
I should be honest about my own part in the detour. The second piece in this thread is a walkthrough of building tools in Google Apps Script, and the design document for this project opened its list of non-negotiable constraints with “Google Apps Script only,” a rule carried over from every tool I build for Compass. The default I was defending was my own habit rather than the problem in front of me.
Why a file?
Most of the advantages of a tool that’s just a file show up after the person who built it has left the room.
There’s nothing in it to expire. No key needs renewing over summer break, no subscription sits in next year’s budget, and no hosting account is tied to someone’s personal email. The previous piece in this thread was about building tools that keep working when the API goes away, and a file skips that question entirely because it never had an API to lose.
The data also stays where it started. The documents are read inside the browser on the machine where they already live. For course content that matters less than it would for student information, but the habit is a good one to build, and for a tool that does touch student data it’s the strongest privacy position available. Telling a district technology director that nothing gets uploaded anywhere is a much shorter conversation than working through a data agreement.
Durability is part of it too. Glitch, a popular home for small web projects, ended project hosting in July 2025, and the apps hosted there stopped running. A copy of a file that someone has already downloaded keeps working no matter what happens to the site that distributed it.
The advantage I care about most is handoff. The security piece in this thread left maintenance and handoff as open questions, meaning what a builder owes the people who inherit a tool. A file doesn’t settle that, but it shrinks the problem considerably. There’s nothing to redeploy or reauthorize, so what someone inherits is the file and a page of instructions.
When is a file the right shape?
Not every tool should be a file, and the ones that should tend to share a recognizable shape.
The best fit is work that transforms something a person already has into something another system accepts. A Word document becomes a Canvas package, a roster export becomes balanced class lists, or a set of observation notes becomes a formatted summary. A person starts the process and a person finishes it, so the tool never has to act on its own, sign in to anything, or remember anything between uses. It also helps a great deal when the inputs are consistent enough that you can write the pattern down, since that’s what lets the logic be ordinary code instead of a model call.
The fit breaks down in predictable places. A file can’t run on a schedule, so anything that should send a reminder every Monday needs to live somewhere that’s awake on Mondays. It can’t easily pool information from several people, which works when a few observers pass files along and falls apart at the scale of a building. It can’t keep a secret either, because anyone can open it in a text editor, so an API key never belongs inside one. The output can carry limits of its own, too. Importing the same package twice in Canvas creates a second copy instead of updating the first. That was acceptable here, because the workflow is to build once and then edit by hand in Canvas, but it would be a poor fit for content that changes every week.
These are the questions I now ask before reaching for Apps Script or a hosted service:
- Does a person start and finish this task anyway? If so, the tool may only need to handle the middle.
- Can the output be a file that an existing system already imports? Canvas, many student information systems, and nearly every spreadsheet program have an import option somewhere.
- Does anything need to happen when nobody is at the computer? Schedules, reminders, and notifications all point away from a file.
- Does the tool need a secret, or information from more than one person? Either one usually means it needs a server of some kind.
If the first two answers are yes and the last two are no, the tool can probably be a file.
How it came together
The practical side is less exotic than it might sound, but a few things cost me time that someone could have saved me.
The first surprise was that a modern JavaScript project split into modules won’t run from a double-clicked file. Chrome and Edge treat module scripts loaded from a desktop file as cross-origin requests and block them, and a tool meant to open anywhere can’t count on other browsers behaving differently. The workaround is to ship everything as one plain script. I keep the code in separate files so I can find things, and a short build script with no dependencies stitches them into one HTML file with the styles, template pieces, and icon images inlined.
The second was deciding which browser features to trust. There’s a newer interface that lets a web page save files straight into a folder on disk, and it’s tempting to use. It works in Chrome and Edge, but Firefox and Safari have both declined to support it. The older pattern works everywhere: read the files a person chooses or drags onto the page, and hand the result back as a download. For a tool headed to whatever computer an agency employee happens to have, boring and universal is the right call.
The third is the one that made the whole change of direction affordable. The Apps Script version had been built in layers with strict boundaries between them. One layer reads the document into a neutral list of pieces (headings, paragraphs, tables, images, markers). The next assembles those pieces into a description of the module that never mentions Canvas. Another renders that description through a template, and only the outermost layers know anything about Google or Canvas. When the platform changed, roughly 1,100 lines of the middle moved across unchanged, and only the reading and writing ends were rewritten. If I could pass along one habit to someone building in Apps Script today, it would be this one. Keep the part of the tool that does the thinking separate from the part that talks to the platform, and a platform change becomes a rewrite of the edges rather than the whole thing.
The fourth was that the preview and the export have to come from the same code. The tool renders each preview page through the identical path it uses for the exported package, changing only how images are referenced. A preview built separately would drift over time, and a preview that misrepresents the output is worse than having none.
Patterns are not free
The previous piece ended on a worry that every deterministic path is a path somebody has to maintain, and that at some point using less AI costs more human effort than it saves. This project is a fair place to test that, and the honest answer is that the pattern-following version took a lot of work.
Real documents are consistent without being clean. The second module used a Word setting that marks a table’s first row as a header without making the text bold, and the tool’s rule for spotting headers looked for bold, so every header cell came through as an ordinary cell with no warning. The same module wrote its equations in Word’s equation editor, which stores them separately from normal text, and they disappeared from the output just as quietly. Neither failure announced itself, and I found both by reading the pages.
That led to the practice I’d recommend most. I generated a test document made up entirely of edge cases, one per page, where the first line of every page states what should appear on it. Import it into Canvas and read down, and any page that doesn’t match its own opening sentence is a bug. Canvas itself becomes the report. Alongside that, the tool flags what it can’t handle instead of guessing. Equations are listed for a person to check rather than rebuilt, and a figure whose image is missing shows up as a visible dashed placeholder rather than vanishing from the page.
This is where the comparison with handing documents to a model is most useful to me. The model did the translation well, and a deterministic tool can fail silently too, as those header rows showed. The difference is in what a fix is worth. When the tool gets something wrong, I fix it once, and that fix holds for every module after it at no added cost. A model starts from scratch on every run, spending the full cost again on work the previous run already figured out. For a one-time job, that trade can be reasonable. For a job that comes back every time a new course arrives, the pattern pays for itself, provided someone actually does the checking.
I didn’t write all of this code by hand. Most of it was written with Claude Code, working from design documents I wrote and kept revising as the project changed direction. That’s the “AI in the building, not in the running” principle from Designing for Human Judgement: a model helped me build a tool that has no model inside it.
What the file gives up
It would be convenient to end on the file as the answer, and it is not quite that.
A hosted tool gets fixed once, and everyone using it has the fix. A file gets copied, and every old copy keeps working exactly as well as it did the day it was downloaded, bugs included. Each version sitting on someone’s desktop is a version I can no longer reach. The practical mitigation is a version number inside the file and one dependable place to download the current release, which is a distribution habit more than a technical solution. For a small team at one agency, that is manageable. For a tool shared publicly with districts I will never hear from, I am less sure, and it is the same maintenance question this thread keeps circling, relocated from a server to a downloads folder.
There is a smaller loss as well. The Apps Script build could have published a module with a single click, and the file asks a person to work through a handful of import screens instead. Those screens are worth what they removed, in my view, and the person clicking through them is standing in the right place. I would still like to hear from anyone who has found a good way to keep scattered copies of a tool current without putting it back on a server. You can reach me at licht.education@gmail.com, and there are more tools, articles, and resources at bradylicht.com.
