People ask me which of my posts to read if they want to get better at product management. I never had a good answer, because I did not write them as a curriculum. I wrote them because something was on fire.
This is the inventory.
The job, as I have been saying since 2015:
Builders create the product. The PM’s job is to generate clarity and commitment so builders can create magic, and to protect one coherent customer experience across the rest of the company.
Everything below is a tool for doing that job. If a post does not help you create clarity, make a choice, or install a mechanism, it is not in the toolbox.
Start here if you think PM means owning the backlog, insulating engineers from customers, or being the smartest person in the room.
Never start with the technology; start with the customer experience…then invent what has to be true.
If it is not written in complete sentences, it is not thought through. If it is not debated early, execution will suck later because folks will be fundamentally misaligned.
A roadmap is not strategy. A long list of “priorities” is peanut butter. Dates must be treated with sanctity.
Good intentions never survive contact with a calendar. If the behavior has to happen when you are not in the room, it needs an owner, a tool, adoption, and inspection.
If you want the index of the mental models themselves, that is Mental Models and Tools to Achieve Clarity of Thought. This post is the product-management cut.
Do not read it top to bottom.
If you cannot explain the PM job without saying “backlog,” start at 1. If you are about to build something, start at 2 and write a PRFAQ. If the room is using the same words and meaning different things, start at 3. If everything is urgent, start at 4. If it only works when you personally hero it, start at 5.
I am putting these under a Product Management category on tig.log so the set is findable as a set. If I missed a tool, tell me.
The post Tig’s Toolbox for Product Management first appeared on tig.log.Max Corbridge, an ethical hacker and red teamer who is co-founder and CEO of Secure Agentics, speaks with SE Radio host Amey Ambade about how AI agents get attacked and what engineers can actually do to defend them. Drawing on years of offensive security work, Corbridge frames agents as a new and largely undefended attack surface: the industry has handed AI systems autonomy and the ability to act in the real world while carrying forward prompt injection, a flaw the frontier labs themselves describe as effectively unsolvable. He likens the moment to the early, lawless days of the web, when SQL injection was everywhere and adoption ran far ahead of security.
The conversation builds from first principles as Corbridge explains what separates an agent from ordinary software and why three properties make them hard to secure: they are non-deterministic, their language-model core can be coerced, and they are increasingly interconnected through MCP servers, other agents, databases, and email. Turning to the attack surface, Corbridge lays out his "lethal trifecta" (a vulnerable core, dense interconnection, and security tooling that has not caught up) and contrasts the decades of layered defenses protecting an ordinary email inbox with the thin protection around agents that take autonomous actions on critical systems.
The heart of the episode is defense. Corbridge orders practices by leverage: least-privilege access and privilege separation, sandboxing where feasible, imperfect-but-useful guardrails as one layer of defense in depth, and human-in-the-loop for irreversible actions (which he notes is contentious and does not scale). The discussion closes on detecting a compromised or drifting agent, the value of watching an agent's chain-of-thought reasoning alongside its actions, the open-source tooling landscape (including Corbridge's own project, Adrian), and his central advice: build security in proactively, define what good agent behavior looks like up front, and avoid bolting it on after agents have already spread across the business.
Grok 4.6 is fast, capable, dramatically cheaper than the leading models—and another sign that AI users have more genuinely strong options than ever. NLW explores how competition from xAI, Chinese labs, and open-weight models is giving individuals and businesses more freedom to choose the right combination of intelligence, speed, and price. In the headlines: massive funding rounds, booming infrastructure demand, and changes to the White House model-testing framework.
The AI Daily Brief helps you understand the most important news and discussions in AI.
Subscribe to the podcast version of The AI Daily Brief wherever you listen: https://pod.link/1680633614
Get it ad free at http://patreon.com/aidailybrief
Learn more about the show https://aidailybrief.ai/
Every release post covers the headline. Thirteen major versions of those leaves a lot of API that shipped in a bullet list and never got explained — which is a shame, because some of it is the stuff I actually reach for most.
This post is the sweep. Three features get real depth because they change how you’d write the code around them, and the rest are quick hits. Nothing here is new in v13; all of it is in the box today.
You have a document in the database and an object in memory that’s been edited by a form, a merge, or an LLM. The question every app asks next is what actually changed? — for an audit line, for a confirmation screen, for an “are you sure, this touches 4 fields” dialog.
The usual answer is to write a comparer. You don’t have to:
var patch = await store.GetDiff<Order>(order.Id, edited);// JsonPatchDocument<Order>, RFC 6902 — null when no such document exists
foreach (var op in patch!.Operations) logger.LogInformation("{Op} {Path} => {Value}", op.Op, op.Path, op.Value);GetDiff reads the stored document, compares it against the candidate, and hands back a
JsonPatchDocument<T> — a real RFC 6902 patch, not a string. Note the shape of it: this is a read. You
haven’t written anything yet, so it’s the thing to call before the save, which is exactly when the
question gets asked.
The write side has the same idea from the other direction. A full Update replaces the stored body; an
Upsert deep-merges it, RFC 7396:
// only the non-null properties are touched; everything else stays as storedawait store.Upsert(new Order { Id = id, Status = "shipped" });That’s the default behaviour of Upsert on every provider. Its inverse — replace-on-update rather than
merge — is Upsert(patch, patchIfUpdate: false), which the relational providers implement and the rest
refuse rather than fake. There’s a matching Update(document, patch: true) when the document must already
exist.
For a genuine partial update through a typed object, remember the type has to be able to express
“unset”: JsonIgnoreCondition.WhenWritingNull on the properties, or use the
JSON collection lane where the body is a JsonObject and absent means
absent.
And when it’s one field, skip the object entirely:
await store.SetProperty<Order>(id, o => o.Status, "shipped");await store.RemoveProperty<Order>(id, o => o.CancelReason);Both return bool — false when no such document — and both are a single statement against the JSON
column. No read, no round trip, no lost update from the write you didn’t know was concurrent with yours.
Map a type as temporal and every one of those writes leaves a version behind:
options.ConfigureDocument<Order>(cfg => cfg.MapTemporal(o => o.Retention = TimeSpan.FromDays(90)));var temporal = (ITemporalDocumentStore)store;
await temporal.History<Order>(id); // every versionawait temporal.AsOf<Order>(id, lastTuesday); // the document as it wasawait temporal.ChangesByActor<Order>("user:42"); // everything one actor touchedawait temporal.GetDiffBetween<Order>(id, 3, 7); // a patch between two versionsawait temporal.Restore<Order>(id, version: 3); // put it backThat’s an audit trail, a point-in-time read, a per-user change log and an undo button, from one line of
configuration. It works on every provider — relational, Cosmos, MongoDB, LiteDB, IndexedDB — through
ITemporalDocumentStore, which you probe for rather than assume (store is ITemporalDocumentStore).
The reason I keep pointing at this one: almost everybody hand-rolls a ChangeLog table, and it’s almost
always worse than this, because a hand-rolled one records the fields somebody remembered to record.
Here’s the shape of an enormous number of API endpoints:
app.MapGet("/orders/{id}", async (string id, IDocumentStore store) =>{ var order = await store.Get<Order>(id); // JSON -> Order return Results.Ok(order); // Order -> JSON});The database handed you a perfectly good JSON document. You parsed it into an object graph, allocated
every string and list in it, and then serialized it straight back into bytes that are — modulo whitespace
— what you started with. Order did no work. It was overhead with a type name.
DocumentDb stores JSON. So take JSON:
app.MapGet("/orders/{id}", async (string id, IDocumentStore store) =>{ var raw = await store.Query<Order>().Where(o => o.Id == id).FirstOrDefaultRawJson(); return raw is null ? Results.NotFound() : Results.Content(raw, "application/json");});And for a list, don’t even materialize the list — stream it into the response as it comes off the reader:
ctx.Response.ContentType = "application/json";await store.Query<Order>() .Where(o => o.Status == "open") .OrderByDescending(o => o.CreatedAt) .WriteJsonArrayTo(ctx.Response.Body, ct);The point that makes this usable rather than a curiosity: you still build the query with the typed
surface. Where, OrderBy, Paginate, global query filters, soft delete, tenancy — all of it applies,
because only the terminal changed. You get the compiler checking your predicate and the database
handing back bytes.
There’s a node lane too, when you want to touch the JSON before it leaves: ToJsonList,
ToJsonAsyncEnumerable, FirstJson / FirstOrDefaultJson, SingleJson / SingleOrDefaultJson,
ToJsonCursorPage — all returning JsonObject, all built on the same RawJsonRows primitive the raw
lane uses.
T to finish the query, so the body is re-serialized
through the type’s JsonTypeInfo. Same JSON, same API — but the round trip is real, and you should
expect no win.DocumentBlob shows up
as its metadata envelope, not its payload.That last group is why there’s a SupportsRawJson flag on the query. Test it rather than catching the
throw when the JSON lane is an optimization and the typed path is still correct — which is exactly how the
built-in OData and AI surfaces pick a lane.
Pair it with the string projection when the caller only wants some of the document:
// a REST ?fields= sparse fieldset, resolved at runtimevar rows = await store.Query<Order>() .Project("id, number, total, customer.name as customer, lower(status) as status") .ToJsonList();Dotted paths reach into nested objects and become first-class output keys. Scalar functions from the
string grammar (lower, length, substring, year, soundex, …) can be projected too, and require an
alias. Relational providers do this in SQL; the document providers do it client-side.
Soft delete shipped in v12 as a one-liner:
options.ConfigureDocument<Customer>(cfg => cfg.AddSoftDelete(x => x.IsDeleted));Deletes set the flag instead of deleting, and every read hides flagged documents. The part worth writing about is that nothing in any store knows it exists. Here is, essentially, the whole implementation:
options.AddInterceptor(interceptor); // cancel the delete, set the flag insteadoptions.AddBulkInterceptor(interceptor); // same for ExecuteDelete / Clearoptions.Mappings.AddQueryFilter("soft-delete", mapping.NotDeleted);Three public calls. No provider changes, no if (softDelete) anywhere in the query pipeline, and it
works identically on all twenty-odd backends because it never went near one.
The two primitives it’s made of are worth knowing on their own.
ctx.Cancel() — replace a write, don’t just watch itAn interceptor’s BeforeWrite can substitute itself for the write:
public class ArchiveOnDelete : IDocumentInterceptor{ public async Task BeforeWrite(DocumentWriteContext ctx, CancellationToken ct) { if (ctx.Operation != DocumentOperation.Delete || ctx.DocumentType != typeof(Order)) return;
var order = await ctx.Store.Get<Order>(ctx.Id!, cancellationToken: ct); if (order != null) { await ctx.Session .Add(new ArchivedOrder { Id = order.Id, Body = order, ArchivedAt = DateTimeOffset.UtcNow }) .SaveChanges(ct); } ctx.Cancel(); // the store performs no delete, and reports success to the caller }
public Task AfterWrite(DocumentWriteContext ctx, CancellationToken ct) => Task.CompletedTask;}Cancel() means the store does nothing, no AfterWrite runs, and no change notification is published —
the caller gets the outcome you name (Cancel(succeeded: false) reports failure). It’s only legal inside
BeforeWrite; calling it later throws rather than silently doing nothing. ctx.Session is scoped to the
write’s own transaction, so the archive row commits with the operation that caused it, or not at all.
That’s an append-only archive, in about twenty lines, that no provider needed to hear about.
A global filter usually gets registered anonymously and then becomes a problem the first time an admin screen needs to see past it. Give it a name:
options.ConfigureDocument<Order>(cfg => cfg.AddQueryFilter("archived", o => !o.IsArchived));store.Query<Order>().IgnoreQueryFilters("archived"); // just this onestore.Query<Order>().IgnoreQueryFilters(); // all of themWhich is exactly what soft delete’s own IncludeDeleted() does — it’s a one-line extension over
IgnoreQueryFilters(SoftDelete.FilterName). Features here are meant to be built this way: extension
methods over public hooks, so an optional feature never becomes a member on an options class that every
provider has to carry.
ToQueryString() — see what your LINQ actually became, without running it.
var q = store.Query<Order>().Where(o => o.Total > 100).OrderBy(o => o.CreatedAt).ToQueryString();Console.WriteLine(q.Sql); // the provider's SQL (or MongoDB's rendered BSON)Console.WriteLine(q.Parameters); // the bound valuesRelational providers and Cosmos return SQL; MongoDB returns its filter as JSON; the in-memory evaluators (LiteDB, IndexedDB) throw, because there’s nothing to show.
Cursor pagination — Skip/Take gets slower the deeper you go and shifts under concurrent writes.
Keyset paging doesn’t:
CursorPage<Order> page = await store.Query<Order>() .Where(o => o.Status == "open") .OrderByDescending(o => o.CreatedAt) .ToCursorPage(cursor, take: 50);
page.Items; // this pagepage.NextCursor; // opaque token; null means that was the last pagepage.HasMore;O(log n) per page with an index on the sort key, an Id tiebreaker appended for you, and a shape hash so
a cursor can’t be replayed against a differently-filtered query. ToJsonCursorPage is the same thing in
the JSON lane. There’s no total count — that’s what Paginate is for.
DocumentFunctions.Soundex — fuzzy name matching that pushes down to the engine:
store.Query<Person>().Where(p => DocumentFunctions.Soundex(p.Name) == DocumentFunctions.Soundex("Smith"));Native SOUNDEX() on SQL Server and MySQL, fuzzystrmatch on PostgreSQL, a registered UDF where there’s
nothing built in.
NotifyOnChange() — a change feed scoped to one query, as an IAsyncEnumerable:
await foreach (var change in store.Query<Order>().Where(o => o.Status == "open").NotifyOnChange(ct)) Console.WriteLine($"{change.ChangeType}: {change.Id}");IDocumentSeeder — versioned, provider-agnostic seed data with a marker so it runs once:
public class ProductSeeder : IDocumentSeeder{ public string Name => "products"; public int Version => 3; // bump to re-run
public Task SeedAsync(IDocumentStore store, CancellationToken ct) => store.BatchInsert(Products, cancellationToken: ct);}
services.AddDocumentSeeder<ProductSeeder>(); // runs at startupJSON Schema validation (Shiny.DocumentDb.JsonSchema) — draft 2020-12, checked against the exact bytes
about to hit disk:
options.ConfigureDocument<Order>(cfg => cfg.MapJsonSchemaFromFile("schemas/order.json"));Schema-free doesn’t have to mean unvalidated, and it’s per type — validate the two documents that matter and leave the rest open.
Computed properties — a value derived from other fields that you can still filter and sort on:
options.ConfigureDocument<OrderLine>(cfg => cfg.MapComputedProperty(x => x.LineTotal, x => x.Quantity * x.UnitPrice, indexed: true));indexed: true asks for a materialized, indexable computed column where the backend has one; without it,
it’s an alias expanded into the query.
IDocumentMaintenance — ClearAll() wipes every type including temporal, spatial and vector sidecars
(tests and dev resets, not tenant-scoped), and SweepOrphanedBlobs<T>() collects blob rows whose owning
document went away out of band. Probe for it: store is IDocumentMaintenance.
The two most under-advertised things in the project aren’t API at all. ShinyDocDbMyAdmin has had a
terminal front end since v12.5 — the same tool as the web UI, as a dotnet tool, over SSH — and the web
one has shipped as a Docker Desktop extension since 13.0.1, which
hands it every database container already running on your machine, connected.
Both deserve their own post with screenshots, and they’re getting one. In the meantime: the admin docs.
In the .NET development world the two most significant frameworks for AI are Microsoft Agent Framework and Microsoft Foundry. Together, they create a powerful ecosystem for building, deploying, and managing AI agents that can automate tasks, respond to user queries, and integrate seamlessly with various services. This post will explore how these two technologies relate to each other, their key features, and their real-world applications.

To quickly review, Microsoft Agent Framework is a development framework designed specifically for creating AI agents. These agents are capable of interacting with various services and data sources, making them versatile tools for developers. The framework provides a rich set of tools and libraries that enable developers to build intelligent applications that can automate tasks, respond to user queries, and integrate with other systems.
For much more on Microsoft Agent Framework and agentics in general see the blog posts beginning here.
Microsoft Foundry is a managed platform that provides a comprehensive environment for building, deploying, and scaling AI agents. It offers a suite of tools and services that enable developers to utilize various AI models and frameworks, making it easier to create sophisticated applications.
The relationship between the Microsoft Agent Framework and Microsoft Foundry is one of synergy and integration. Together, they provide a robust environment for developing AI agents that can handle complex tasks and workflows. Here are some key aspects of their relationship:
The Foundry Agent Service acts as a bridge between the Microsoft Agent Framework and various data sources and other agents. This integration enables seamless communication and data exchange, which is crucial for developing multi-agent workflows that can manage complex business processes.
Additionally, the Responses API serves as a single entry point for accessing Foundry models and tools. This allows developers to build agents using the Agent Framework while leveraging the capabilities of Foundry, creating a more cohesive development experience.
Both Foundry and Microsoft Agent Framework support the creation of multi-agent workflows. This feature allows developers to orchestrate complex, multi-step processes, enhancing the capabilities of AI applications. By enabling multiple agents to work together, organizations can automate intricate workflows that would be challenging to manage with a single agent.
Security is a paramount concern in the development of AI applications. The Microsoft Agent Framework utilizes Microsoft Entra ID for managing agent identities, ensuring secure authentication and authorization. With Foundry you get this and many other infrastructure features out of the box.
The combination of the Microsoft Agent Framework and Microsoft Foundry offers developers significant flexibility in how they create agents. They can choose to build agents using the Microsoft Agent Framework and have them hosted in Foundry, or they can create AI applications directly with Foundry. This flexibility is essential for meeting the diverse needs of businesses and organizations.
For more on this, be sure to watch my video interview of Bruno Capuano and Jon Galloway, both of Microsoft, where, among other things, Bruno demonstrates how easy it is to have Foundry host a Microsoft Agent Framework application.