Signal blog Official Microsoft Blog Command Line Microsoft On The Issues Asia Canada Europe, Middle East and Africa Latin America The Code of Us What's new AI Innovation Digital Transformation Sustainability Security Work & Life Diversity & Inclusion Unlocked Microsoft 365 Azure Copilot Windows Surface XBOX Deals Small Business Support Windows Apps Outlook OneDrive Microsoft Teams OneNote Microsoft Edge Moving from Skype to Teams Computers Shop XBOX Accessories VR & mixed reality Certified Refurbished Trade-in for cash XBOX Game Pass Ultimate PC Game Pass XBOX games PC games Microsoft AI Microsoft Security Dynamics 365 Microsoft 365 for business Microsoft Power Platform Windows 365 Small Business Digital Sovereignty Azure Microsoft Developer Microsoft Learn Support for AI marketplace apps Microsoft Tech Community Microsoft Marketplace Software companies Visual Studio Microsoft Rewards Free downloads & security Education Gift cards Licensing Unlocked stories View Sitemap

By builders, for builders.

A Microsoft publication

Nine engineers, 1,211 pull requests in a day: Lessons from building Copilot Cowork

Plus a case study where we put everything together by moving our runtime from Python to Rust while live.

In January of this year, a small team at Microsoft set out to build Copilot Cowork, an agentic execution layer that performs multi-step workflows on a userโ€™s behalf. That result brings the powerful AI-assisted tools and capabilities developers have today to everyday knowledge workers, all with Work IQ, Microsoft 365 security and governance, cloud execution, and Microsoftโ€™s multi-model architecture. 

This is the story of how the team took Copilot Cowork from an empty repo to worldwide GA in about four and a half months, then moved its runtime from Python to Rust while it was live. Agents wrote most of the code. Ways of working changed along the way, and the practices other teams can adapt to build like a frontier development team: moving fast while keeping reliability, security, and cost in view. 


On October 21, nine engineers merged 1,211 pull requests into the main branch of the Copilot Cowork backend. That number would have sounded absurd to the team in January, when a handful of us made the first commit to an empty repo.   

Agents wrote most of the code, but engineers remained responsible for defining the work and judging the results. Our small team wrote clearer specs, set architecture and security boundaries, and focused its review on higher-risk changes, production impact, and cases where checks or reviewers disagreed. Automated checks and agent reviewers examined every change. Agents could propose changes, but they could not decide what was allowed to happen. 

We didnโ€™t plan on working this way. But every few weeks, the agents got better, part of our process stopped working, and we adapted. 

Six practices emerged from those changes. They cover how we specify work, run agents within clear boundaries, test our checks, preserve what the team learns, shape the codebase for parallel work, and set limits on automated loops. They arenโ€™t a recipe for matching our PR count. They are lessons other teams can adapt to build like a frontier development team while keeping reliability, security, and cost in view. 

Weโ€™ll end with the project that puts those practices to the test together: moving the live runtime from Python to Rust. 

Lesson 1: Write the spec before the code 

The first commit was made on January 13. From the start, agents wrote the code. 

In the first weeks, we learned that the agent was rarely what slowed us down. The bottleneck was not knowing exactly what we wanted. A vague request got us code that looked reasonable, survived a quick read, and then did the wrong thing. Fixing it took longer than writing a proper request would have in the first place. 

So we adopted a rule that still stands today: no code without a markdown spec written first. The spec covers three things: 

A good spec saves far more time than it costs upfront. But a weak one leads to rework a few days later. The faster agents became at implementation, the more important it became for us to be precise about intent, requirements, and what good looked like. 

Many of our specs start out spoken. Someone talks through a feature, and an agent turns that into a first draft of the spec, which the human then edits. I think this did more for our speed than any single model upgrade. 

We launched Copilot Cowork to our first enterprise customers in the Frontier program on March 30. 

Lesson 2: Run narrow agents inside clear boundaries 

The next step was running more than one agent at a time. Each works in its own copy of the repo, on one task, from one spec. One agent became five, and five became dozens. 

Merged PRs per month went from 257 in January to 2,754 in May. Our busiest week before GA had 783. 

Figure A. Pull requests merged into main per month in 2026, with Frontier (March 30), GA (June 16), and the Rust cutover (September 23 โ€“ October 10) marked. October covers the 1st through the 20th.
Figure A. Pull requests merged into main per month in 2026, with Frontier (March 30), GA (June 16), and the Rust cutover (September 23 โ€“ October 10) marked. October covers the 1st through the 20th.

Parallel work went better than we expected for two reasons: 

In Copilot Cowork, the model runs in a container without user credentials. A trusted host outside the container handles identity, policy, approvals, and tools, and every Microsoft 365 operation goes through the host.  

We didnโ€™t want to limit what the agent could attempt, because a heavily restricted agent isn’t very useful. With our architecture, the model can plan and ask for anything, and the host decides what actually happens.  

That separation lets us increase the number of agents working in parallel without giving the model credentials or direct authority over operations. The host still applied identity, policy, approvals, and audit at the point of execution.

Figure B. The agent container holds no user credentials and has no direct network path to Microsoft 365. Every request crosses the boundary to the trusted host, which owns identity, policy, approvals, tool execution, and audit.
Figure B. The agent container holds no user credentials and has no direct network path to Microsoft 365. Every request crosses the boundary to the trusted host, which owns identity, policy, approvals, tool execution, and audit.

Building that in at the start was much easier than adding it later wouldโ€™ve been. It also helped in a way we didnโ€™t plan for. The agent only talks to the host through a defined interface, so we could later replace the entire host without changing the agentโ€”which we did when we migrated to Rust. 

We reached worldwide general availability on June 16, about four and a half months after the first commit, with several models and 16 plugins. 

Lesson 3: Make checks prove they checked something 

With agents writing most of the code, nine people couldnโ€™t read every change, and waiting for a human review was becoming the slowest step for every PR. 

Several engineers each built their own review bot, modeled on how they review code themselves, and the bots competed. When one bot caught a problem another had missed, the bot that missed it got a new rule. Over time, each reviewer built up a large set of rules learned from its own misses. Each one has now been through at least10,000 generations; in each generation, the reviewer runs reviews, then reflects on what it missed and gets better at reviewing code. 

Today, a useful first review usually arrives within several minutes of a pull request being opened. On a sample of recent merged PRs, the median was about four and a half minutes. 

Slower, more specialized passes follow: 

The author gets quick feedback while the change is still fresh, and the detailed reading still happens. 

Of course, reviewer agents can still miss things. Once, a group of reviewer agents read a change in one of the most sensitive parts of the product. Each had been told to try to break it, and none found a problem. People on the team then read the same diff and found four blocking issues, all of which were fixed before it merged. 

Fast first feedback was useful, but it wasnโ€™t permission to skip judgment. We kept agentic review as the first check and added at least one human check for critical changes. We made it clear which findings could block a merge. 

We also split findings into two tiers: 

Another rule came from a check that couldnโ€™t fail. We keep a size limit on the instruction files agents read, because every extra byte costs tokens on every run. The limit was exceeded, and the check that enforced it stayed green the whole time. 

The script piped the checkโ€™s output through tail. A shell pipeline returns the exit code of its last command, and that was tail, which always succeeded: 

``` # The pipeline's exit code is tail's, so this always passes lint-size-check | tail -1 # Keep the check's own exit code and print it lint-size-check > out.txt 2>&1; rc=$?; echo "lint rc=$rc"; cat out.txt; [ "$rc" -eq 0 ] || exit 1 ```

Once we went looking, we found the same problem in other places: 

We wrote a two-part rule: 

I think this matters more with agents than without them. Agents keep working until the checks pass, so if a check can pass without testing anything, an agent will eventually find that path. 

Lesson 4: Put what the team learns in the repo 

By August, the same mistakes kept coming back. Each agent session started without any memory of the previous one, so we moved the teamโ€™s rules into the repo: 

Now, every agent reads the relevant files when it starts work. When something goes wrong, the fix usually includes a new or updated rule. There are about 30 rule files now, and the oldest dates from August 6. 

One early rule was about merge conflicts. At a few hundred merges a day, almost none of our conflicts were in actual logic. They were in files every branch appended a line to: registries, lists, generated files, and documents with a โ€œlast updatedโ€ line. Instead of asking people to resolve conflicts more carefully, we changed the files: 

Lesson 5: Shape the codebase for parallel work 

Agents also wrote most of the first Rust code in the repo, and they put things wherever it was easiest, which usually meant the biggest crate. At one point, the main runtime binary was about 290,000 lines, and splitting it up took a long time. 

On August 20, we took stock of the Rust workspace: 

For a team running many agents in parallel, that structure was a problem. Most changes touched the same few crates, rebuilt most of the workspace, and conflicted with each other. 

We fixed it with a strict rule about roles. Every crate is one of four kinds: 

Implementations may depend on contracts and kits but not on other implementations. Only binaries depend on implementations, and binaries contain wiring code only. When one implementation needs something from another, that dependency goes through a contract. 

New code gets no exceptions and no allowlists. Each exception we had granted in the past had been used to justify the next one. 

The cleanup ran as a program of 401 work items with 2,225 dependencies between them, and at the peak, 40 to 44 items were ready at once. We grouped work into batches that never touched the same files, so parallel agents wouldnโ€™t conflict. We even split the gateโ€™s own list of temporary exceptions into one file per crate. Otherwise, a single wave of about 20 agents would all have edited the same file. The engineer who owned the program made decisions at set checkpoints, and agents did most of the implementation. 

The CI gate started out as a report. Then it became a ratchet, where the allowed number of exceptions could only go down. The results: 

Figure C. The Rust workspace before (August 20) and after (October 7). Before: one deep chain, with business logic in the binary and implementation crates depending on each other. After: binaries only wire things together, and implementations depend only on contracts and kits.
Figure C. The Rust workspace before (August 20) and after (October 7). Before: one deep chain, with business logic in the binary and implementation crates depending on each other. After: binaries only wire things together, and implementations depend only on contracts and kits.

The build-time gains were smaller than we had predicted. Our compile cache already hit about 99% of the time on pull request builds, so the run-to-run variation in build time was bigger than the improvement. What we could measure was structural. After a change to a base crate, the longest chain of crates that must be rebuilt in sequence went from 11 to 3. This is now used to drive further speed improvements and prevent compilation time from increasing linearly with the size of the codebase. 

Lesson 6: Give automated loops targets and limits 

For most of this year, the loop was manual: a person prompted an agent, read the result, and decided what to do next. More of our work now runs as automated loops. A harness gets one measurable target and keeps working until it reaches it or runs out of attempts. People choose the target, decide what must not get worse, and keep the final say on anything that reaches production. 

Test speed 

The clearest example is a harness whose target is the time CI spends running tests. It opens two kinds of pull requests: 

Its pull requests only merge if CIโ€™s own timing data shows the touched tests at least 5% faster. A review step also compares every assertion against the original, so a faster test canโ€™t be one that checks less. About 300 of these PRs have merged since mid-September. Across them, the touched test files went from roughly 17,700 seconds of CI time to about 1,700. 

Document quality 

We ran a series of passes, each of which changed the skill that generates documents. Each pass was judged by comparing its output with the previous version on a fixed set of tasks. A pass was kept only if all three of these held: 

If one of those criteria wasnโ€™t met, it was reverted. Three of 10 passes were kept. Some passes won the head-to-head comparisons and were still rejected, because their weakest outputs had gotten worse. A person made the final decision to ship. 

The factory: Production signals in, measured merges out 

Our newest harness, which we call the factory, takes the long tail of engineering work as its input: an error cluster a release introduced, feedback that points at a tool, a main build that broke an hour ago, a file that grew past its size budget. Each one is small. Together they arrive faster than people can pick them up. Deterministic rules turn those signals into work items, so nobody files tickets, and we donโ€™t trust a model to raise the alarm. The harness does not need access to customer content to improve the system; it reproduces issues using the logs and the userโ€™s feedback as context. 

Each item starts from a production numberโ€”for example, 2,148 rejected payloads across 688 conversationsโ€”and includes an acceptance check that the harness runs itself and a test that fails before the change. The item then goes through the stages a person would follow: research, a design, a build, and a pull request. Every stage that matters has a critic running on a different model family from the agent it checks. 

Before a pull request opens, the harness reverts the fix and runs the new tests again. If a test still passes without the fix, it isnโ€™t considered evidence, and the change doesnโ€™t go out. Itโ€™s the same rule as the one we wrote for our checks, applied to every change the harness makes. 

Merging isnโ€™t the finish line either. The harness follows each merged commit into a release and the first production ring, then checks whether the signal actually dropped on builds that contain the fix. If the problem comes back, it becomes a new work item. 

In the week of October 12, 4,581 pull requests merged into main, about 650 a day, and a growing share of them came from the factory. 

All of the factoryโ€™s state lives in one database, so any machine can pick up any run, and a crash loses a process, never a fact. 

Every loop we run has four things decided before it starts: 

Case study: Moving the runtime from Python to Rust 

Python got us to GA quickly, and I donโ€™t think we wouldโ€™ve made the June date with anything else. At the scale we reached afterwards, though, it caused three problems: 

We first tried moving blocking work off the event loop. That helped, and it showed that the underlying limit was the single loop itself rather than any individual blocking call. 

Each of the earlier changes played a part in the migration: 

But it was hard to know whether a port was correct. Reading Rust next to Python doesnโ€™t scale, so we recorded what the Python did instead. 

For each behavior we port, a small generator runs the real Python code over a set of inputs and records the results: response bodies, status codes, log lines, and storage writes. That output is a JSON file we call a fixture. The Rust test replays the fixture and has to match it exactly. 

Nobody edits fixtures by hand. Every generator can also run in a check mode, which fails if the fixture no longer matches the Python. We have about 430 fixtures. 

This is a small one, for the function that decides whether a turn is still running: 

``` // fixture: written by running the Python, never edited by hand { "cases": [ { "has_pending_redelivery": false, "signal_routes_in_flight": 0, "awaiting_up_request_ids": [], "running_turn_request_id": "", "last_terminal_request_id": "", "expected": true }, ... 47 more ] } #[test] fn every_python_case_is_reproduced() { let fixture: Fixture = serde_json::from_str(FIXTURE).expect("fixture parses"); assert_eq!(fixture.cases.len(), 48, "the generator's full grid"); for case in fixture.cases { assert_eq!(turn_running(&case.inputs()), case.expected); } } ```

The test checks the number of cases before comparing them, because a replay over zero cases would also pass. 

When the Python is deleted, its last fixture stays in the repo as the specification for the Rust code. Where the Rust is allowed to behave differently, the difference goes on a list of known gaps that can only get shorter. 

We moved traffic one cluster at a time, behind flags that could switch back. We finished on October 10โ€”the day after we deleted the Python.

DateStep
September 23First internal attempt, rolled back
September 24Rust served its first production turns
October 9Python runtime deleted: About 8,000 files and 4.9 million lines, including tests and generated code
October 10Last cluster moved

Time to first token on the first turn of a session dropped a lot. These numbers compare a Rust cluster with a Python cluster over the same 12-hour window in late September:

Language p50p90
Python7.9 s14.5 s
Rust2.7 s6.2 s

Not all of that benefit comes from Rust. The same change moved the agent loop out of the container, which was slow to start, and into the Rust service. I think that move accounts for a large part of the gain. 

The event-loop stalls and the 5-second setups are gone. We havenโ€™t measured cost per turn properly yet, so weโ€™re not making any claim about cost. 

Two things went wrong: 

We fixed both. 

What the six lessons add up to 

The lesson isnโ€™t to aim for our PR count. Itโ€™s to move human attention to the decisions and controls that matter more when code arrives faster. Most of our own time now goes into four decisions: 

The Python-to-Rust migration tested these practices together. We could move quickly because we had specified the work, recorded behavior to check against, and moved traffic in steps we could reverse. 

Some of these practices are especially useful when you can define what โ€œdoneโ€ means, changes arrive faster than people can review them, or you can compare a replacement system with one that is still running. But the broader lesson applies beyond those conditions: engineers didnโ€™t step out of the process. They spent more time deciding what the boundaries are, where human review was essential, and what was ready for production. Weโ€™re exploring how agents can handle more of the routine merge workflow while preserving explicit human checkpoints for higher-stakes changes. 

Try it yourself 

You donโ€™t need to start with dozens of agents.  

Start with one check your team trusts: make it fail on purpose and confirm that it does. Then choose one bounded task, write down what success means and what must not break, and see what changes when an agent works within those limits.