Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
157091 stories
·
33 followers

Windows 11 rejuvenation list just got longer, with more legacy dialogs headed to WinUI 3

1 Share

Marcus Ash, who leads Design and Research for Windows and Devices at Microsoft, has confirmed that the “Switch to a local account” dialog, the one that still tells you to use the Windows 8 “Search charm,” is now on the company’s list for rejuvenation.

The confirmation came after a user posted a screenshot of the dialog from Windows 11 25H2 build 26200.8655 on X, tagging Ash. His reply also says that Microsoft is working on updating a long list of classic dialogs in Windows 11.

Windows 11 showing Windows 8 style dialog while switching to local account in Settings

Windows 11 still tells you to use a Search charm that died with Windows 8

If you have ever switched your Microsoft account to a local one on a device with BitLocker or device encryption turned on, you have probably seen this dialog before backing up your recovery key.

Windows 11 mentions Charms bar
Windows 11 mentions Charm bar

It reads, “To back up your recovery key, close this dialog box and use the Search charm to search for “device encryption.”

The Search charm hasn’t existed since Windows 8.1. It was part of the Charms Bar, a strip of five icons (Search, Share, Start, Devices, and Settings) that showed up when you swiped in from the right edge of the screen or pressed Win+C. Windows 10 killed the Charms Bar in 2015, replacing it with taskbar search and the modern Settings app.

Charms bar

Unsurprisingly, this isn’t the first time someone has noticed Microsoft being lazy to update an old dialog. Back in 2023, we reported that Windows 11’s Settings still referenced the Windows 8 Charm feature. Three years later, the dialog is unchanged with the same wording.

The X post that reignited the conversation came from a user who spotted the dialog on 25H2 build 26200.8655 and tagged Ash, joking that “someone missed this dialog for rejuvenation.” Windows historian Albacore chimed in too, pointing out that apart from the old design, the phrase “Search charm” itself is the giveaway that tells the language is from an interface that stopped existing a long time ago.

Microsoft still shows Windows 8 Search charm while trying to switch to a local account

Marcus Ash replied that the dialog is now “on our list of rejuvenation surfaces,” and continued with the same vibe by attaching a meme about how the Windows 8 reference makes people feel old.

Marcus Ask reply about Switch to local account dialog being rejuvenated

It’s not hard to guess why this particular dialog slipped through the cracks for so long. Microsoft has never been enthusiastic about local accounts to begin with, and the company has spent years nudging, and at times outright forcing, people toward Microsoft accounts during setup. A screen that only shows up when someone tries to leave that ecosystem was never going to be a priority.

Every legacy dialog Microsoft is being rewritten for Windows 11

“Rejuvenation” is Microsoft’s internal shorthand for modernizing old UI surfaces (dialogs, icons, wording, and layouts) that hasn’t received the Fluent Design treatment Windows 11 promised back in 2021. Based on what we’ve tracked over the past several months, the local account dialog is just the latest addition to a list that keeps getting longer.

One of my favorite examples is the Run dialog. Windows Latest reported in April that the hidden modern version of Win+R was getting a slimmer design ahead of rollout, and by May, Microsoft had detailed the engineering behind it. The new Run box is built with WinUI 3 and .NET AOT compilation, and it loads faster than the Windows 95-era version it’s replacing, 94 milliseconds versus 103ms on the legacy tool, according to Microsoft’s own telemetry.

New Windows 11 Run dialog

It’s still an opt-in feature buried in Settings > System > Advanced. But the fact that Microsoft managed to make a modern Run that is faster than the fully optimized legacy Run gives me more hope in this “rejuvenation”.

File operation dialogs got there first. Copy, move, delete, and cut dialogs picked up dark mode support months ago, and Microsoft has since confirmed that the file copy dialog has been fully rewritten in WinUI 3. March Rogers, Partner Director of Design at Microsoft, said as much on X in May, when a user pushed back on the idea that modernizing Run was enough while dialogs like the common file open box were still stuck in Win32. “We are working through our list of all older dialogs and rewriting them in WinUI 3,” Rogers wrote. “The file copy dialog is already done, the common file dialog is on our list.”

Windows 11 dark mode with colours

The File Explorer Properties dialog is next in line. Windows Latest found references to modern “DeletedFileProperties” strings quietly added to File Explorer’s resource files back in May, a strong signal that the Windows 95-era Properties box, the one that still refuses to respect dark mode, is being rebuilt in WinUI 3 too.

File Explorer Properties Dialog box doesn't have a dark mode
File Explorer Properties Dialog box doesn’t have a dark mode

Rogers has had to answer for old UI showing up in new places before. In April, a user flagged that the input method switcher on the Windows 11 login screen still used Windows 8-style design. Diego Baca, Windows Design Director, picked up the thread and promised to pass it along, and Rogers confirmed it had been added to the team’s internal “craft list.”

Windows 11 login screen has Windows 8 elements while changing keyboard layout
Windows 11 login screen has Windows 8 elements while changing keyboard layout

We still have WinRE, the recovery environment you land in after a failed boot, and the old “Please wait” screen with its spinning dots, both of which still look like they escaped from 2012.

Windows Recover Environment looks like it is from the Windows 8 era
Windows Recover Environment looks like it is from the Windows 8 era
Please wait screen with rotating bubbles
Please wait screen with rotating bubbles

Back in March, responding to a similar thread about gaps in dark mode, Ash wrote that Microsoft “started with extending dark mode in the Run dialog and various File Explorer surfaces,” and that the team is “building out tooling to scale modernizing other dialogs across Windows 11 that were built in legacy frameworks.” That last part is the important bit suggesting that Microsoft building a repeatable process for finding and replacing old Windows elements.

Interestingly, rejuvenation isn’t limited to visual aesthetics. Ash also mentioned in a separate exchange that the sound designer who worked on the original Windows 11 startup chime has rejoined his team, which hints at a refreshed set of system sounds down the line. Windows 11’s notification tones, error alerts, and other system sounds haven’t really changed since launch, so it would be exciting to hear what Windows 11 would sound like.

Windows 11 is finally cleaning up after itself

You might be thinking that none of these fixes are glamorous, and I agree. Microsoft should’ve removed all Windows 8 references before their Windows 11 keynote. But now, we’re getting a solid idea of where Windows 11 is headed in 2026.

We’ve been tracking such belated fixes elsewhere in the OS, too. Microsoft finally addressed File Explorer’s cluttered right-click menu with a new Split Context Menu design.

New Windows 11 right-click context menu in File Explorer takes just half the vertical screen space
New Windows 11 right-click context menu in File Explorer takes just half the vertical screen space

The company is rebuilding the Start menu on native WinUI instead of the web-based components that made it feel sluggish. As WinUI takes helm, more parts of the UI will be smoother, like how the File Explorer scrolls smoothly in some views and not others.

That said, the local account dialog was never going to break anyone’s workflow. It’s just wording nobody bothered to update since the Obama administration. But Ash confirming it within days of being tagged, and it already being part of the “rejuvenation” list along with Run, file dialogs, and the Properties box, is a decent sign that Microsoft is keeping it’s promise of fixing Windows 11.

The post Windows 11 rejuvenation list just got longer, with more legacy dialogs headed to WinUI 3 appeared first on Windows Latest

Read the whole story
alvinashcraft
38 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Improvements to reading Process outputs: Exploring the .NET 11 preview - Part 5

1 Share

In this post I talk about some of the important updates to the Process APIs in .NET 11. Adam Sitnik has been working on a whole load of improvements, improving both the "simple" scenarios (such as running a process and reading the output), as well as adding new features (such as fire-and-forget or kill on parent exit). This work all builds on top of fundamental features introduced in early .NET 11 previews.

In this post, I talk about the improvements made to reading child-process output, as these improvements are a great way to avoid some critical failures like deadlocks. I look at how you need to handle it in .NET 100, how it works in .NET 11, and how the new APIs work behind the scenes.

There are many more improvements and new APIs than just these in .NET 11 which you can read about in this blog post from Microsoft.

Reading Process output in .NET 10

Until recently, the .NET Process API had been remarkably stable. There have been a few minor improvements over the years, such as the Process.WaitForExitAsync() method introduced in .NET 5, which allows waiting on a Task for a process to exit, but most of the recent improvements have been behind the scenes, such as reducing allocations or latency of operations. However, there were still many things about the Process API that were harder than they should be, or outright impossible.

Risking deadlocks with ReadToEnd()

One of the classic examples of this is how easy it is to incorrectly start a process and read the standard output (stdout)/standard error (stderr) from the process. For example, the following shows how not to do it:

// Define the process to start
var startInfo = new ProcessStartInfo("git")
{
    RedirectStandardOutput = true,
    RedirectStandardError = true,
};
startInfo.ArgumentList.Add("status");

// Create a Process object and start it
using Process process = Process.Start(startInfo)!;

// ⚠️ Read the output from the process (don't do this, it can deadlock!)
string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();

// Wait for the process to exit
process.WaitForExit();

The problem with the code above is that it can cause deadlocks😬 This happens because:

  • The process writes text to stdout and/or stderr.
  • The call to StandardOutput.ReadToEnd() keeps reading from the stdout, until the pipe is closed, which typically happens on process exit.
    • In the code above, this means the the call above blocks until the process exits, because the stdout reading is blocked until then.
  • The call to StandardOutput.ReadToEnd() will read all of the stderr, but it only starts reading once the process exists.
  • However, if the process writes to stderr, and writes more than the buffer can hold, the child process will also block, waiting for the output to be drained.
  • We're now in a deadlock:
    • The parent won't start reading stderr until the process exits.
    • The child won't exit until the parent reads from stderr.

There are two main solutions to avoiding deadlocks in this scenario, but in both cases, the important thing is to drain both pipes in parallel.

Using Tasks to avoid deadlocks

The simplest way to write the previous code while avoiding deadlocks, is to rely on the asynchronous versions of the methods, ReadToEndAsync(). These return a Task, which will ultimately run on the thread pool in the background, without blocking execution (until you await the Task):

// Create a Process object and start it
using Process process = Process.Start(startInfo)!;

// Start Tasks to read both pipes, but don't await them
Task<string> outputTask = process.StandardOutput.ReadToEndAsync();
Task<string> errorTask = process.StandardError.ReadToEndAsync();

// Wait for _both_ operations to complete _and_ the process to exit
await Task.WhenAll(outputTask, errorTask, process.WaitForExitAsync());

// It's now safe to await the tasks, as you now they're completed
string output = await outputTask;
string error = await errorTask;

The important thing about the above approach is that the tasks run in the background, draining the stdout and stderr buffers, which removes the risk of deadlocks. This is likely the easiest approach, but it won't work for all scenarios.

Using events to read output as it's written

The Task approach is often the easiest way to avoid deadlocks, but it won't work for you in all cases. For example, at work, we often need to react to what's being written to stdout and stderr as it's written, rather than waiting for the process to exit, so we use a slightly different approach based on event-handlers, as shown below:

// Create a Process object and start it
using Process process = new () { StartInfo = startInfo };

// Create types to store the output
StringBuilder stdOut = new();
StringBuilder stdErr = new();

// Create a callback to handle new data being written 
static HandleOutput(string data, bool isStandardError)
{
    var buffer = isStandardError ? stdErr : stdOut;
    buffer.AppendLine(data);

    // Do anything else you need to with the data
}

// Set up the callbacks
process.OutputDataReceived += (_, e) => HandleOutput(e.Data, isStandardError: false);
process.ErrorDataReceived += (_, e) => HandleOutput(e.Data, isStandardError: true);

// Start the process
process.Start();

// Start reading from the pipes
process.BeginOutputReadLine();
process.BeginErrorReadLine();

// As we're draining both pipes, there shouldn't be any risk of deadlock
process.WaitForExit();

This works, and it's about the best we can do with the .NET 10 APIs, but it's safe to say that it's more code than I'd like to write!

Addressing these shortcomings are some of the most visible improvements in .NET 11.

Reading Process output in .NET 11

There are multiple APIs in .NET 11 for improving each of these scenarios:

  • Process.ReadAllText()/ Process.ReadAllTextAsync()
  • Process.RunAndCaptureText() / Process.RunAndCaptureTextAsync()
  • Process.ReadAllLines() / Process.ReadAllLinesAsync()
  • Process.ReadAllBytes() / Process.ReadAllBytesAsync()

I'll walk through each of these API pairs in this section, and show how they safely simplify the above code.

Reading all stdout/stderr into a string

The Process.ReadAllText() and Process.ReadAllTextAsync() APIs cover probably one of the most common scenarios, and are the replacements for the StandardOutput.ReadToEnd() approach I showed previously which could cause deadlocks. There are both synchronous and asynchronous versions, but with the new APIs, both of these are safe to call:

// Create a Process object and start it
using Process process = Process.Start(startInfo)!;

// Read both output and error from the process
(string output, string error) = process.ReadAllText();

// Wait for the process to exit
process.WaitForExit();

Given we're executing synchronous code, the runtime does all the hardwork to avoid deadlocks, by draining both pipes in parallel. On Windows, this relies on overlapped IO with wait handles, while on Unix, it relies on poll-based multiplexing with non-blocking reads. The details of how this are implemented aren't important here (unless you're interested in that sort of thing!) the important thing to understand is that it's safe to call.

The async version of the method is very similar, and you can immediately await the result, unlike when doing that manually. The ReadAllTextAsync implementations handles spawning the Tasks and waiting on them itself, so you can just write the simple code you would expect to write:

// Create a Process object and start it
using Process process = Process.Start(startInfo)!;

// Read both output and error from the process
(string output, string error) = await process.ReadAllTextAsync();

// Wait for the process to exit
process.WaitForExit();

There aren't any overloads for these methods, but you can also provide a timeout (for the sync method) or a cancellation token (for the async method) to ensure you don't sit waiting forever if the process hangs:

public class Process
{
    // ReadAllText has an optional timeout parameter
    public (string StandardOutput, string StandardError) ReadAllText(TimeSpan? timeout = default);

    // ReadAllTextAsync has an optional cancellationToken
    public Task<(string StandardOutput, string StandardError)> ReadAllTextAsync(CancellationToken cancellationToken = default);
}

If the timeout is exceeded (or the cancellationToken is cancelled), the read will throw an exception, so make sure to handle that case. I'd recommend always providing these values (even if they're very big) wherever possible.

These new APIs handle the simple cases where you just want to grab the whole output from the process, but as I described previously, sometimes you need to observe the output as it's generated.

Reading stdout/stderr as it's produced

I described earlier how I had to use the event-based APIs(🤮) at work so that we could view text as it's produced and react to it in real-time, instead of waiting for the process to exit before we handle all the data.

As a reminder, the ReadAllText and ReadAllTextAsync methods will keep reading until the pipes are closed, which typically only happens when the process exits.

In .NET 11, we have a much nicer way to do the same thing. Instead of having to set up the callbacks and configure the listeners, we can use the more idiomatic foreach support for IEnumerable and IAsyncEnumerable with the new Process.ReadAllLines() and Process.ReadAllLinesAsync() APIs.

The following example shows how you can use the ReadAllLines() sync API to read data from the buffer one line at a time, and handle each line, based on whether it came from stdout or stderr.

// Create a Process object and start it
using Process process = Process.Start(startInfo)!;

// Create types to store the output, the same as before
StringBuilder stdOut = new();
StringBuilder stdErr = new();

// Read lines, one at a time, as they're produced
foreach (ProcessOutputLine line in process.ReadAllLines())
{
    // The bool StandardError indicates if this is stderr or stdout
    var buffer = line.StandardError ? stdErr : stdOut;
    // Content contains the actual read data
    buffer.AppendLine(data.Content);

    // Do anything else you need to with the data
}

// Wait for the process to exit
process.WaitForExit();

If you compare this usage to the .NET version that uses events you can see this is much clearer. It's easier to follow, and it's hard to get wrong; you just use foreach and loop over everything!

The synchronous ReadAllLines methods use the same synchronous mechanisms as before (overlapped IO on Windows, or poll-based multiplexing with non-blocking reads), and will block the thread until all the data has been read, and the pipes are closed. In contrast, the async approach uses Channels to handle the async producer/consumer model, without blocking the thread, and uses IAsyncEnumerable<> instead:

// Create a Process object and start it
using Process process = Process.Start(startInfo)!;

// Create types to store the output, the same as before
StringBuilder stdOut = new();
StringBuilder stdErr = new();

// Read lines asynchronously using `await foreach`
// 👇 This is the only change required compared to the sync version
await foreach (ProcessOutputLine line in process.ReadAllLinesAsync())
{
    // The bool StandardError indicates if this is stderr or stdout
    var buffer = line.StandardError ? stdErr : stdOut;
    // Content contains the actual read data
    buffer.AppendLine(data.Content);

    // Do anything else you need to with the data
}

// Wait for the process to exit
process.WaitForExit();

The parity here is very nice too; the sync and async versions, while very different in implementation, are virtually identical when you need to actually use them. That's a big bonus for usability in my eyes.

As before, you can also specify a timeout or cancellation token to ensure you don't block forever if the child process hangs:

public class Process
{
    // The sync version has an optional timeout..
    public IEnumerable<ProcessOutputLine> ReadAllLines(TimeSpan? timeout = default);

    // ...and the async version has an optional cancellation token
    public IAsyncEnumerable<ProcessOutputLine> ReadAllLinesAsync(CancellationToken cancellationToken = default);
}

Both of the methods return a simple readonly struct, as you've seen above:

public readonly struct ProcessOutputLine
{
    public string Content { get; }
    public bool StandardError { get; } // True if the content is from stderr, otherwise false
}

These four new APIs should be your go to replacements for most of the existing code you have where you need to read stdout or stderr from a process, but .NET 11 isn't just about providing better APIs for existing functionality, it also provides niceties for new functionality too.

Reading stdout/stderr as bytes

In my experience, if I want to read a process output, I typically want the string output, but that's not necessarily the case. It might be that you want the raw bytes (in whatever encoding the process is using) so that you can process them yourself. .NET 11 caters for these APIs by providing Process.ReadAllBytes() and Process.ReadAllBytesAsync(). These work very similar to the ReadAllText() and ReadAllTextAsync() versions:

// Create a Process object and start it
using Process process = Process.Start(startInfo)!;

// Read both output and error from the process
(byte[] output, byte[] error) = await process.ReadAllBytesAsync();

// Wait for the process to exit
process.WaitForExit();

These APIs are effectively used by ReadAllText() and ReadAllTextAsync() under the hood, with the text versions performing an additional encoding to string after reading the bytes. Otherwise, you use the APIs in essentially the exact same way.

Making running a process and capturing the text

The sequence of calls above, where you run a process and capture all the text it outputs is very common. In fact, it's so common that .NET 11 adds new APIs specifically for this sequence, to make it easier and simpler. The RunAndCaptureText() and RunAndCaptureTextAsync() methods fill this gap in one easy call:

// Create a Process object, start it,
// capture the output, and wait for exit, all in one!
ProcessTextOutput result = Process.RunAndCaptureText(startInfo);

// ...or run it async
ProcessTextOutput result = await Process.RunAndCaptureTextAsync(startInfo);

This one-shot method call captures everything that we've seen in the previous sections, in one easy line. The ProcessTextOutput includes all the common things you might want to capture, namely the process ID, the stderr/stdout outputs, and the exit codes.

public sealed class ProcessTextOutput
{
    public ProcessExitStatus ExitStatus { get; }
    public string StandardOutput { get; }
    public string StandardError { get; }
    public int ProcessId { get; }
}

public sealed class ProcessExitStatus
{
    public int ExitCode { get; } // If the process was terminated by a signal on Unix, this is 128 + the signal number.
    public PosixSignal? Signal { get; } // Always null on Windows, only has a value if terminated by a signal on Unix
    public bool Canceled { get; }
}

These APIs won't necessarily cover all use cases, but I'd bet they cover at least 90% of scenarios. To simplify things further, there are also overloads that don't require creating a ProcessStartInfo object, and there's the timeout and cancellation options too:

public class Process
{
    // The API we've seen already
    public static ProcessTextOutput RunAndCaptureText(
        ProcessStartInfo startInfo, TimeSpan? timeout = default);
    
    // This version just requires a file name to run
    public static ProcessTextOutput RunAndCaptureText(
        string fileName,
        IList<string>? arguments = null,
        System.TimeSpan? timeout = default);

    // This is the async version
    public static Task<ProcessTextOutput> RunAndCaptureTextAsync(
        ProcessStartInfo startInfo, CancellationToken cancellationToken = default);
    
    // This version also requires a file name to run
    public static Task<ProcessTextOutput> RunAndCaptureTextAsync(
        string fileName,
        IList<string>? arguments = null,
        CancellationToken cancellationToken = default);
}

There's a lot more to come to the Process APIs in .NET 11 (just see Adam's post for lots more details)! That's all I'm going to cover in this post though, so enjoy trying out the new APIs!

Summary

In this post I described some of the new APIs introduced in .NET 11 for running processes and for reading stdout and stderr. I described the problem with the APIs available in earlier versions of .NET, namely that it's possible to cause deadlocks if you don't use the APIs correctly! I then introduced each of the new pair of APIs added in .NET 11 for making this easier:

  • Process.ReadAllText()/ Process.ReadAllTextAsync() for reading all the output from a process.
  • Process.ReadAllLines() / Process.ReadAllLinesAsync() for enumerating each line of output from a process as it's emitted.
  • Process.ReadAllBytes() / Process.ReadAllBytesAsync() for reading all the output from a process as raw bytes.
  • Process.RunAndCaptureText() / Process.RunAndCaptureTextAsync() for starting a new process, reading the output, and then waiting for the process to exit, all in one API.

These APIs make working with processes that much easier in .NET 11, though there are many more improvements I didn't discuss here that you should explore!

Read the whole story
alvinashcraft
38 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Building a Scroll-Driven 3D Gallery Using a Blender Camera Path with Three.js and GSAP

1 Share
Build a cinematic, scroll-driven 3D gallery using a camera path authored in Blender, rendered with Three.js, and animated with GSAP.
Read the whole story
alvinashcraft
38 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Are You Building the Right Thing: The Metrics That Measure How Fast You Learn

1 Share
Are You Building the Right Thing: The Metrics That Measure How Fast You Learn

In the last article, we made the case for the technical signals that tell you whether your teams are getting better at building software. Near the end, we admitted those signals have a limit. They tell you whether you’re building software well. They don’t tell you whether you’re building the right software.

This article is about that second question.

How do I know my teams are building the right thing?

It’s a harder question, and a more uncomfortable one. Deployment Frequency, Lead Time, Change Failure Rate, and Mean Time to Restore measure your delivery engine. A strong DORA score means that engine is fast and reliable. It doesn’t mean the engine is pointed at the right target.

That’s the trap. A fast delivery engine aimed at the wrong thing doesn’t save you. It just means you build the wrong thing faster, and at greater cost. And with AI making teams quicker still, that risk is getting bigger, not smaller.

The real question isn’t whether you can know the right thing up front. You usually can’t. The real question is whether your team can find out, fast and cheap, before you’ve over-invested in the wrong one. That question, technical metrics answer well.

The Right Thing Is Discovered

Most of the time, nobody knows the right thing up front.

A well-known Standish Group study found that 45% of features in shipped software are never used at all, and another 19% are used only rarely. Most of what teams build doesn’t earn its keep. And it isn’t because the teams were careless. It’s because the right thing is genuinely hard to know in advance.

The right thing is discovered, not specified. You build a version, put it in front of real users, watch what happens, and adjust. Then you do it again. The teams that build the right thing aren’t the ones with the best up-front plan. They’re the ones with the fastest, cheapest way to find out they were wrong.

So “are we building the right thing?” is almost the wrong question to ask your teams. They can’t answer it with certainty, and neither can you. The better question is this: how quickly would we find out if we weren’t? That question has a measurable answer.

What Uninformed Progress Looks Like

When a team has no real learning loop, progress looks great.

Teams mark features done. The roadmap moves. The status reports are green. From a leader’s seat, it looks like a team executing well.

We call this uninformed progress. The team is moving, but nobody actually knows if they’re moving toward something customers want. They’re building on an assumption made months ago, and they won’t test it until the big release. By the time the truth arrives, the wrong thing is already built.

This is especially dangerous in a rewrite or a rebuild. A rebuild feels safe because the destination seems known. You’re just rebuilding what already exists. But a rebuild is actually a rare chance to ask a question almost nobody asks: is this ten-year-old feature still needed at all? Teams that skip that question carry a decade of unexamined assumptions straight into the new system.

Uninformed progress is exactly what the next four metrics are designed to expose.

Four Metrics for How Fast You Learn

Here are four metrics we watch to tell whether a team can actually find out it’s wrong. Two measure how fast the learning loop turns. Two measure what the loop reveals.

How fast the loop turns

Batch Size

This is how much scope a team builds before a real user can react to it. A small batch is a thin slice of a feature. A large batch is optimism hiding uncertainty. The larger the batch, the bigger the assumption. Ship small, and you find out you took a wrong turn in a week. Ship big, and you find out after a quarter, or after a two-year rebuild. Batch Size and DORA’s Deployment Frequency move together: if you deploy often, you can ship small; if you deploy rarely, every release carries more risk and more assumption. The leader’s question: are we learning in small bets, or betting big on confidence?

Time to Feedback

This is how long it takes from starting a piece of work to learning something from a real user. It builds directly on DORA. Lead Time for Changes measures how long from code committed to code in production. Time to Feedback picks up where Lead Time ends and keeps the clock running until reality has a chance to respond. Teams that learn fastest aren’t necessarily smarter. They just let reality into the room sooner. The leader’s question: how long can we stay wrong before we notice?

What the loop reveals

Feature Usage

This is the share of what you’ve shipped that people actually use, measured from instrumentation in the product, not from opinion in a meeting. It’s one of the most direct measures of whether you’re building the right thing. A feature that ships and gets no traffic is often the wrong thing, made visible. Those Standish numbers stop being abstract statistics and become a list of specific features you can name. Use is proof of value, not shipping. The leader’s question: do we care enough to check whether anyone actually uses what we built?

Rework Rate

This is how much recently shipped work gets redone soon after it ships. In the last article, churn was a code-health signal. Here it’s the same measurement read through a different question. When teams rework fresh code heavily right after release, that’s rarely a coding problem. It usually means the team didn’t understand the problem before they built. Some rework means the team is listening. Endless rework means they’re guessing. The leader’s question: are we refining what we shipped, or rebuilding it?

A Real Example

A few years ago we had a team that rebuilt a mobile point-of-sale app for a payments company.

The old app was more than ten years old. It was iOS only, which forced the company’s customers to buy expensive Apple hardware when cheaper Android devices were right there. It had no automated tests and a tangled, complicated codebase. Customers were leaving for competitors.

The company’s own estimate to rebuild it was two years. Two years before a single customer would see anything new. Two years of watching customers leave. And there was strong pressure to make the rebuild a perfect copy. Many people insisted the new app had to match the old one feature for feature before anyone could use it.

We didn’t do that. Instead, we built the simplest complete point-of-sale we could think of, for the simplest possible customer: a cash-only donut shop. That first release was ready in about four months, not two years. Then a taco truck, a slightly more complex quick-serve case. Then up the ladder, one customer type at a time, toward a full sit-down restaurant.

Each step was small enough to put in front of a real business. An actual restaurant ran each new version for a week and handed back what worked, what broke, and which options they missed. That is Batch Size and Time to Feedback working together.

The old app carried more than 700 configuration options, built up over a decade. The instinct was to rebuild every one of them. But because real customers were using the new app at every stage, we could see which options actually got used. Between 200 and 300 of those 700 turned out not to matter enough to build before the final release. We didn’t argue about them in a conference room. The usage data settled it.

None of this would have worked without a fast, reliable delivery engine underneath. The team integrated their work dozens of times a day and could release on demand. The new code was simple and carried close to 5,000 automated tests that ran hundreds of times a day. So when feedback said change course, changing course was cheap. It was an adjustment, not a rewrite.

The rebuild shipped about 25% faster than the company’s own two-year estimate, and across more than 1,000 features built, real customers hit only about 20 minor defects. But the thing we think about most isn’t a number. The staged approach bought us a conversation with the client. Every release, we sat down together and asked what the next kind of restaurant actually needed. We weren’t guessing in a room. We were deciding with evidence.

Why This Matters Even More with AI

In the last article, we argued that AI amplifies the engineering system you already have.

AI is collapsing the cost of building. A team with good AI tooling can produce a working feature far faster than it could two years ago. That sounds like pure good news. It isn’t.

When building gets cheap, building the wrong thing gets cheap too. The constraint moves. It used to be that building was the hard part. Now the hard part is knowing what is worth building at all. AI makes the learning loop the whole game. If your team can generate features faster than it can find out whether anyone wants them, you don’t have a productivity win. You have a faster way to fill your product with dead weight.

A Practical Next Step

You don’t need to interrogate your teams about whether they’re building the right thing. They can’t prove it, and pressing them will only produce confident guesses.

Ask the loop questions instead. At your next review, ask how big the last few releases were, and how long it took before a real user touched them. Ask which shipped features have the usage to prove they were worth building, and which ones nobody can vouch for. Ask whether recent work is being refined or rebuilt.

If your team can’t answer those questions, that is the finding. It means the learning loop isn’t instrumented, and uninformed progress is the most likely thing happening right now.

One caution. These metrics measure whether your team can learn fast. They don’t make anyone act on what’s learned. A fast loop feeding a roadmap nobody is willing to change is just a quicker way to be ignored. The metrics are the easy part. Listening is the leadership part.

Because building the right thing was never about getting the plan right up front. It’s about how quickly you’re willing to find out you were wrong, and how cheaply you can do something about it.

This post comes from our software engineering practice, which specializes in refactoring application architecture and optimizing delivery to support modular teams, faster feedback, and continuous value delivery.

Read the whole story
alvinashcraft
38 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Hugging Face Models on Foundry Managed Compute

1 Share
Read the whole story
alvinashcraft
39 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Agent Skills for .NET Is Now Released

1 Share

You can now give your .NET agents reusable packages of domain expertise – instructions, reference documents, and scripts they load only when a task needs them – through a stable, production-ready API. Agent Skills for .NET in Microsoft Agent Framework has moved out of experimental preview – the [Experimental] attribute is removed and the API is stable. Teams can build skills, ship them independently, and combine them in any agent, with the governance controls enterprises need before putting agents into production.

If you’ve been following our earlier posts on file-based skills and authoring modes with script execution, everything described there is now stable and shipping.

What are Agent Skills?

Agent Skills is an open format for packaging domain expertise that agents discover and use on demand. Each skill has metadata and instructions – a SKILL.md file for file-based skills, or equivalent properties in code – optionally accompanied by scripts, reference documents, and other resources. The agent loads only what it needs, when it needs it, keeping the context window lean through a four-stage progressive disclosure pattern: advertise skill names → load instructions → read resources → run scripts.

The result is agents that gain specialized capabilities without bloating their core instructions or their context window – and expertise you author once and reuse across every agent that needs it.

For a full introduction to the format, see Give Your Agents Domain Expertise with Agent Skills.

What you can do with it

Enforce enterprise policy consistently

Package your company’s HR policies, expense rules, or IT security guidelines as skills. An employee-facing agent loads the relevant policy skill when someone asks “Can I expense a co-working space?” and answers from the policy itself – without every policy sitting in context at all times. Because the skill drives a repeatable, auditable workflow, every employee gets the same grounded answer.

Turn support playbooks into repeatable workflows

Turn your support team’s troubleshooting guides into skills. When a customer reports an issue, the agent loads the matching playbook and follows the documented steps – so resolution is consistent regardless of which agent instance handles the request.

Compose skills from multiple teams

Different teams can author and maintain skills independently – as file directories in a shared repo, or as packages on your internal NuGet feed – and you combine them into one agent without cross-team coordination. The agent decides which skill to use based on each skill’s description; you write no routing logic.

A great place to find some existing skills is in the Awesome Copilot repo for skills.

Three ways to author skills

The release supports three authoring styles, so each team can pick what fits how they work. All three plug into the same provider, and the agent treats them identically at runtime:

File-based skills – A directory with a SKILL.md, optional scripts, and reference documents. Good for skills that live in a shared repo and are maintained by non-developers or cross-functional teams.

Class-based skills – C# classes that package instructions, resources, and scripts for distribution through normal .NET workflows, including internal NuGet packages.

Code-defined skills – Skills created directly in application code. Useful when a skill needs to be generated dynamically or close over application state.

Built for production

Giving an agent new capabilities is only useful if you can govern how it uses them. This release includes the controls you need to run skills in production.

Human-in-the-loop approval. The skills provider exposes three tools the agent calls to work with skills – load_skill (load a skill’s instructions), read_skill_resource (fetch a bundled resource), and run_skill_script (execute a bundled script). All three require approval by default, so nothing loads or executes without oversight – and you can relax it selectively for trusted operations.

Controlled script execution. Class-based and code-defined skill scripts run in-process. File-based scripts are delegated to a runner you provide, so you own sandboxing, resource limits, and audit logging.

Filtering. Expose only a curated subset of a shared skill library to a given agent, with a predicate that can make context-aware decisions based on the requesting agent or tenant.

Caching. Skills are resolved once and reused, with optional per-key isolation so one provider can serve different skill sets to different agents or tenants.

Extensible source pipeline. The underlying source classes are now public, so when the builder doesn’t fit your needs you can compose custom pipelines or integrate skills from your own registries.

As with any dependency that can influence agent behavior, review skill content before deployment, sandbox file-based scripts, and log which skills, resources, and scripts are used.

Getting started

Install the required .NET packages, then wire a skills provider into an agent:

using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;

var skillsProvider = new AgentSkillsProvider(
    Path.Combine(AppContext.BaseDirectory, "skills"),
    SubprocessScriptRunner.RunAsync);

AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
    .GetResponsesClient()
    .AsAIAgent(new ChatClientAgentOptions
    {
        Name = "MyAgent",
        ChatOptions = new() { Instructions = "You are a helpful assistant." },
        AIContextProviders = [skillsProvider],
    },
    model: deploymentName);

AgentResponse response = await agent.RunAsync("Help me with onboarding.");
Console.WriteLine(response.Text);

Why this matters

Agent Skills gives you a standard way to package, distribute, and govern domain expertise for your agents. Teams author skills independently, the builder composes them into a single provider, and approval keeps a human in the loop for anything that matters. Now that the .NET API is stable and released, you can build on it in production without the churn of an experimental API.

The post Agent Skills for .NET Is Now Released appeared first on Microsoft Agent Framework.

Read the whole story
alvinashcraft
39 minutes ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories