How Projects became Custom Agents

What it took to turn a saved setup into something people could reuse, share, and run on its own.

In December 2024, we started building a feature called Projects in AI Drive. The idea was simple: put your instructions, models, and files together once, and come back to that setup whenever you needed it. You could start a new conversation without setting everything up again.

That feature became Custom Agents. Today, people use it for repeatable work in AI Drive, and our own team builds and shares agents for work they come back to regularly. Seeing that happen is one of my favourite parts of having worked on it from the early versions.

But there was a lot between those two points. A setup that worked for its creator in one open tab had to keep working when someone shared it, renamed it, reopened an old conversation, or scheduled it to run without them. Most of the engineering work was in making those transitions reliable. This is the story of that work, including the assumptions we had to go back and change.

Starting with something you could come back to

The first Projects implementation brought together an instructions editor, a model selector, a knowledge base, and separate chat history. I worked across the frontend and backend to connect those pieces. The saved setup and the conversations using it needed to stay related, while still being separate things.

Early testing made that distinction very practical. Removing a file mention could erase the whole line of instructions. The model selector looked like a single-choice control even though you could choose several models. Chat titles showed a file name when the Project name would have helped you recognise the work. We fixed those behaviours, added file selection and drag and drop, and kept testing how it felt to create a Project and return to it.

Returning to a Project also meant restoring the right conversation context. Later, we caught a case where finishing a chat could switch the history panel into a custom agent's history without the user asking for it. I changed that flow to respect the active Project. A tab knowing which agent it used did not mean it should take over the history the user was currently viewing.

The name changed, and the data model had to catch up

In March 2025, we started calling Projects Custom Agents. The team was already thinking about task-specific agents, prebuilt agents, and scheduled work. The new name described where the feature was going. We still had to explain it clearly because there was already an Agent option in the chat composer.

One problem during that transition looked like a model-picker bug. You could select Sonnet in agent mode and then see the normal chat version appear too. Editing an older Project could also produce the wrong model choice. I tried to preserve compatibility with the saved Projects while fixing the selector, but the problem kept coming back.

After working through it with the team, we found the important detail: the tab-opening flow passed models around as an array of strings. Chat and agent variants could share the same model name. We were trying to recover a distinction that the data had already lost. I changed the flow to carry structured model objects with their mode, while continuing to handle the older saved format. Folder selection also needed to normalise those old values and retain models that supported agent behaviour.

That experience stayed with me. What looked like a small dropdown change was really about preserving the meaning of a saved choice across the editor, storage, and chat.

Sharing changed what the setup needed to know

We stored agent definitions as .gpt files under System/Project, which let us build on AI Drive's existing sharing infrastructure. I added shared-file discovery with a *.gpt filter. But sharing the definition was only the beginning of making an agent usable by another person.

Testing exposed team-sharing errors, including agents with no attached files. Shared chat-history lookup could return a 404. Switching agents could briefly display the previous agent's configuration. We fixed the sharing cases, corrected the history handling, and introduced a loading state while the next configuration loaded. A recipient needed their own conversation history, even though the agent definition belonged to someone else. We later made the sharing UI distinguish between being allowed to use an agent and being allowed to clone it, showing the clone action according to the recipient's role.

We also found an assumption in discovery: shared agents could disappear if the user had no personal Projects. Those lists needed to work independently. And public links needed an agent-specific opening experience. We initially disabled that sharing option because opening a configuration file in a viewer was not yet a useful way to start working with the agent. That capability came in later steps.

The knowledge base revealed a deeper version of the same problem. An instruction could refer to docs/report.pdf. For the creator, the location was obvious. For a recipient, that relative path could point into their own Drive. The text had stayed the same while its meaning had changed.

The same document, with its owner made explicit

docs/report.pdf~owner_user_id/docs/report.pdf
Illustrative path. The owner identifies the resource; access checks still decide whether the person running the agent can read it.

I first worked on preparing references in the shared-agent frontend flow. Later, we made knowledge-base ownership explicit in saved paths and normalised older paths when the backend resolved an agent. A path that already named another owner kept that owner. The June 2026 work supplied those resolved paths as separate runtime context, leaving the author's instructions intact. Reading or searching a file still required permission from the user running the agent.

Even migrating the saved files needed care. We limited the lazy migration to the user's own definitions, preserved metadata, and used a storage generation precondition when overwriting the content so a concurrent edit could not be silently replaced. This did not rewrite existing chat histories; the new runtime context applied to new chats.

A clone is not a rename

Once people could reuse an agent, editing and copying became much more important. Early clones kept the source name, which confused later save and delete operations. Adding a copy suffix helped distinguish them, but there were more assumptions underneath that behaviour.

In one early edit flow, we deleted the old definition before uploading its replacement. I moved that cleanup and history-folder rename after the successful upload. We also added ownership checks so editing a copy of a shared agent would not be treated as renaming the source. The name alone was not enough to decide which file could be changed.

Another version surfaced in August 2026. Cloning an agent opened an editor with a name ending in “Copy”. If you changed that name before saving, the code treated it as a rename and tried to delete the old “Copy” file. That file had never existed. The delete returned a 404 and the new agent was never uploaded.

I made clone mode an explicit exclusion from the existing-agent rename check and added regression coverage for clones, owned renames, shared sources, and older definitions. We needed to know whether a persisted object existed, rather than infer that from two different names.

We also found a mismatch between where an agent was saved and where My Agents looked for it. The upload succeeded, but the agent appeared to be missing. The team fixed that by keeping agent definitions in the creator's personal agent folder. A successful write was only half the job: the save and discovery paths had to agree.

Making reuse work across the product

As the feature grew, people could find agents through shared links, featured collections, search, favourites, and recently used lists. I worked on those entry points too. They made reuse easier, but also gave us more places where configuration or state could become stale. We added explicit refresh after changes, prevented concurrent featured fetches, and centralised validation so different views agreed on whether an agent had usable data.

There was some straightforward performance work here as well. The grid repeatedly searched the full featured-agent list to match each sorted item back to its definition. I replaced those repeated searches with a map lookup. For recently used agents, we replaced multiple synchronous storage calls with one asynchronous listing filtered to chat files. These changes reduced unnecessary work in the paths people used to get back to an agent.

The editor had its own consistency problem. After instructions moved into a document-viewer iframe, typing @ could activate a hidden editor's suggestion menu. Selecting a file updated the knowledge base without updating the visible instructions. I brought typed mentions, the Mention button, and file drops onto the same token and knowledge-base handling. In Markdown, a file mention also had to behave as one unit when moving the cursor, deleting, or undoing, while keeping the full reference in the saved text.

Meanwhile, opening the editor could no longer be a prerequisite for getting the agent configuration right. In January 2026, I moved custom agent chat toward sending an agent_path instead of the full instructions from the frontend. The backend resolved the saved definition and checked access to shared agents. Older conversations needed a compatibility path: where possible, we reconstructed the reference from the chat-history location and handled renamed agents. The backend support had to ship before the frontend depended on it.

This gave the saved definition a clearer role. It was the configuration the rest of the product could resolve, rather than something that depended on whichever screen happened to open the conversation.

Running the agent when nobody is there

Scheduling took the original idea a step further. You could prepare the work once and have it run later, without returning to the app. I worked on the scheduling interface in late 2025, including one-time and weekly schedules, time zones, model selection, and next-run and last-run information.

That interface needed to handle more than a successful save. We used cancellation and request tracking to stop an old response from updating the wrong agent after a quick switch. An optimistic toggle had to revert if the API failed. We added validation to prevent saving a scheduled agent without an agent-capable model, and a way to remove a schedule completely. A visible setting needed to describe what was actually configured to run.

Later, I found a mismatch between interactive and unattended execution. The frontend saved canonical connector IDs in the agent definition, but the trigger resolver still treated those values as older application names. A scheduled run could continue without the connectors selected in the editor. I updated the resolver to accept both formats, deduplicate references, and retain the ownership, entitlement, and active-status checks. Email-triggered agents benefited from the same resolver change.

Then there was the output. Automated runs could fill the conversation history and make manual chats harder to find. I added a separate filter for scheduled results with unread visibility. Making an agent run in the background also meant making its work easy to find when someone came back.

What grew out of Projects

By June 2026, I was putting custom agents our team used regularly onto our new-chat screen. Colleagues were building agents for support work and model information, then sharing them for others to use or adapt. Customer conversations were also reaching us through support, including problems with agent context and email-triggered work. The setup we had started with was becoming part of how people expected to get work done.

Looking back, the useful engineering lesson is how much information a reusable setup has to carry. Which model mode was selected? Who owns the files? Is this a new copy or an existing agent? Which conversation is being resumed? Can an unattended run resolve the same tools? Those details became more important every time we made the feature available in another place.

We started with Projects because we wanted people to stop setting up the same work repeatedly. Custom Agents grew from following that idea through sharing, editing, discovery, and automation. The name changed along the way. The work was making the saved setup dependable wherever people chose to use it.