Aspire 13.3 adds a notification center to the dashboard: a bell-icon panel that catches every command response with a markdown body and a clickable “View response” entry. It is small, it is dev-loop sized, and it is the thing that finally makes resource commands worth writing for your local AppHost instead of just for ops.
The toast problem
You click “Restart” on a resource. A toast slides in: “Command executed.” It fades. Did it actually restart, or did it fail and the toast carried a sad face you missed? You scroll back through logs to find out.
Toasts are ephemeral by definition. They optimise for not being in the way, which is the opposite of what you want when you’re poking at a running app on your own machine. The dashboard has needed a record of what commands did for a while, and 13.3 ships it as a notification center behind a bell icon in the dashboard chrome.
What ships
Two things, and they compose:
- A notification center. A bell icon in the dashboard header opens a panel of recent command results. Each entry has the resource name, the command display name, a status intent (info, success, error), and a “View response” action that opens the full body in a dialog.
- Markdown rendering for command messages. The
Messagestring on a command result renders as CommonMark in the panel: tables, code blocks, and links all work. Each entry’s “View response” panel opens the body in the dashboard’s text-visualizer dialog.
Together they turn WithCommand from a fire-and-forget button into something that gives you operational context inside the dashboard you already had open.

A dev-loop example: rebuild as a real command
The first place this earns rent is somewhere you didn’t expect: the built-in rebuild command. In 13.3 it returns the build output as a text command-result, which means clicking “Rebuild” on a project resource and then “View response” in the bell shows you the actual dotnet build log inline. No alt-tab to the terminal, no scrolling Aspire’s chunked log pane, no wondering if the rebuild even succeeded. The command knows what it did, and the bell preserves the answer until the next time you look.
That’s the shape every command should have: do the thing, return enough body that the developer can confirm it worked, and let the structured payload feed scripts and CLI separately. Here is a minimal example that follows it for a seed-test-data command, the kind you actually want a confirmation from because you’re about to run a flaky test against the data it just produced:
demoService.WithCommand(
name: "seed-test-data",
displayName: "Seed test data",
executeCommand: async ctx =>
{
var result = await SeedAsync(ctx.CancellationToken);
var markdown = $"""
## Seeded test data
| Entity | Count | Time |
|---|---|---|
| Tenants | {result.Tenants:N0} | {result.TenantsMs}ms |
| Orders | {result.Orders:N0} | {result.OrdersMs}ms |
| Events | {result.Events:N0} | {result.EventsMs}ms |
Total {result.TotalMs}ms. Run `reset-test-data` to roll back.
""";
return CommandResults.Success(
summary: $"Seeded {result.Tenants} tenants, {result.Orders} orders, {result.Events} events",
data: new CommandResultData
{
Value = markdown,
Format = CommandResultFormat.Markdown
});
});
Click “Seed test data” once: the bell entry tells you exactly what got created, in roughly the order you’d ask about it. The same command runs from the CLI:
$ aspire resource demo-service seed-test-data
✅ Command 'seed-test-data' executed successfully on resource 'demo-service'.
Seeded test data
┌─────────┬───────┬─────────┐
│ Entity │ Count │ Time │
├─────────┼───────┼─────────┤
│ Tenants │ 50 │ 312ms │
│ Orders │ 1,247 │ 2,108ms │
│ Events │ 8,400 │ 894ms │
└─────────┴───────┴─────────┘
Total 3,314ms. Run reset-test-data to roll back.
The CLI parses the markdown body and renders it as a terminal table; the dashboard renders the same body as rich markdown. Same Data.Value, two surfaces, one implementation. Set Format = CommandResultFormat.Json instead and the body emits as raw JSON in both places, ready to pipe through jq.
The same surface works for an LLM coding agent that shells out to aspire resource demo-service seed-test-data. Markdown rendering reads fine in both terminals and LLM context windows, but if the consumer is mostly an agent or a script, set Format = CommandResultFormat.Json and skip the markdown-table heuristics: the body comes back as raw JSON, ready for jq or a tool call. One implementation, three readers: human in the dashboard, script in CI, agent during pair-coding. The CLI only ever shows the current command’s response, though; there’s no aspire notifications list or equivalent, so the bell remains the only place to scroll back through past results.
By default the bell entry waits until you click “View response.” Set DisplayImmediately = true on the CommandResultData and the dialog pops the moment the command completes, skipping the bell-click step:
return CommandResults.Success(
summary: $"Seeded {result.Tenants} tenants, ...",
data: new CommandResultData
{
Value = markdown,
Format = CommandResultFormat.Markdown,
DisplayImmediately = true,
});
Use it for results you’re about to act on right now: a freshly-provisioned URL you’ll copy, a screenshot path heading into a ticket, a one-time secret. Don’t use it for periodic results; the dialog gets old fast. The previous post in this series leans on this flag for the screenshot command, where the file path is the whole point of running it.


Failure results render the same way, with the panel header reflecting the command name and the error intent, and the body keeping its formatting:

HTTP endpoints can join the same pipeline
If your service already has a diagnostic HTTP endpoint, you do not need to write a WithCommand lambda for it. WithHttpCommand wraps the endpoint as a command, and 13.3 lets you choose how the response surfaces in the bell:
api.WithHttpCommand(
path: "/admin/tenant/describe",
displayName: "Describe tenant",
endpointName: "http",
commandName: "describe-tenant",
commandOptions: new HttpCommandOptions
{
Method = HttpMethod.Get,
ResultMode = HttpCommandResultMode.Auto
});
Auto picks Markdown vs JSON vs Text based on the response’s content type. Json and Text force the format. The default HTTP method is POST, so set Method explicitly when wrapping a GET endpoint. The endpoint itself stays a normal route; the dashboard treats it like any other command. This is mostly useful when a service already exposes ops-shaped endpoints behind an internal-only port.

Where this changes how to design commands
Pre-13.3, resource commands were mostly admin actions: restart, flush cache, replay DLQ. Things that do something. The bell makes the case for those stronger by letting confirmation land inline (like the seed-test-data example above) and unlocks a second category that wasn’t really viable before: read-only inspection commands whose payoff is the rendered message itself. A few I’d add to a real AppHost:
describe-tenant {tenantId}returns a markdown summary of a tenant’s queue depths, last-seen timestamps, and feature flags. Faster than firing up a Postgres console when a teammate asks “what’s the state of tenant 42 on your machine?”recent-errorsreturns the last N errors with markdown links to the traces that contain them. Faster thanaspire logspiped through grep, and the link rendering is what makes it nicer instead of just different.config-snapshotreturns the current effective config of a service, rendered as a code block with the source of each value (env, default, override). The markdown code block keeps it readable; the sameData.Valuelets you diff two snapshots from the CLI.
The other knob worth knowing about when designing your own commands is UpdateState on commandOptions. It’s a callback that returns Hidden, Disabled, or Enabled per resource state, and it’s how built-in commands like Restart know not to show on a resource that’s already stopped. Use it on your own commands to keep the actions menu honest: gate seed-test-data behind the database resource being healthy, gate replay-dlq on the queue being reachable. The bell’s job is to tell you what happened; UpdateState decides whether the developer can fire the command in the first place.
The anti-pattern: turning the bell into a chat log. If every command writes a paragraph, you stop opening the panel. Keep messages dense, table-shaped where it helps, link-heavy where the next step lives elsewhere. And do not write a notification for a state transition the dashboard already shows; the resource state column already tells you a thing went from running to stopped, the bell should add information, not duplicate it.
Persistence: session-scoped, by design
The bell holds the most recent 100 entries in memory inside the dashboard process, and a refresh clears them. There is a “Dismiss all” action; there is no cross-reload persistence and there is no “export” button. That sounds like a gap until you remember what this is for.
The dev loop runs aspire run, fires a few commands while iterating, and exits. The bell is the journal of that session. Closing the AppHost and starting again should feel like starting fresh, because the state in your services is also fresh. If you need a permanent record of what happened, that belongs in a file, an ADR, or a real ops tool. The notification center is not pretending to be that.
The implication for command design is small but real: do not lean on the bell as a place readers come back to days later. Lean on it as the thing your future self in two minutes will look at to confirm what just happened.
Display names where it counts
Lifecycle command messages used to read apiservice-2hf9 restarted successfully, with the DCP-internal suffix for replicated resources. 13.3 cleans this up for single-instance resources: the message now reads apiservice restarted successfully. Replicated resources still keep the suffix, because they are individual instances and need it to be unambiguous. So the rule of thumb: the display name in the bell matches the display name in the resources list when there is one of the thing, and falls back to the suffixed form when there are several.
If you have ever had to explain to someone why the dashboard shows apiservice in the resource list and apiservice-2hf9 in a command response that is, in fact, the same thing, you will appreciate the change. It is also better for screen readers and for any external tool that scrapes the response.
Outcome
The bell turns “did that command actually work?” from log-spelunking into a one-glance answer for the rest of the current aspire run. Pair it with the structured Data.Value channel and a single command implementation can satisfy a human in the dashboard and a script at the CLI at the same time, with no second implementation and no second format. That is the upgrade. Use it for inspection and quick-confirmation commands during the inner loop; do not pretend it is an ops surface, because it isn’t.
The first post in this series, WithBrowserLogs, put a tracked Chromium next to your frontend resources and streamed console plus network into the same dashboard. The bell is what makes that stream readable instead of merely present, and it is the thing every command in the rest of this series leans on.