Summary#
DateTime.UtcNow and DateTime.Today read the current instant, anywhere. Pasted C# that reads "now" runs
unchanged.
Inside a workflow or durable body, DateTime.UtcNow already reads the instant the ENGINE is processing at — the
same one every clock armed on that pass derives from, and the one a pinned test clock moves. DurableClock.Now /
.UtcNow / .Today are the same read under a name that says so, and the only difference between the two is where
each is legal: DurableClock outside a durable execution fails loudly, because there is no engine instant for it to
promise. A body that is RE-ENTERED from the top — a retry: step's next attempt, a run moved to a new version — reads
either spelling afresh; an instant that must survive that is recorded in a step:
var at = Workflow.Once("stamped-at", () => DateTime.UtcNow);. osy lint points this out where it matters: the rule
workflow-value-read-again-on-retry fires when a clock read (or a Guid.NewGuid()) taken before a retry: step is
then written to a row, and suggests exactly that Workflow.Once. A read that is only logged or compared is not flagged.
There are three clocks in total, and each is legal exactly where it means something:
| you want | write | where |
|---|---|---|
| the engine's instant, replay-stable | DateTime.UtcNow, or DurableClock.UtcNow to insist on it | a workflow or durable body |
| a one-shot reading of now | DateTime.UtcNow | anywhere |
| a value that KEEPS MOVING on screen | DateTime.UtcNow | anything that runs on a RENDER PASS — a render block, and any method render calls. See below |
Reach for the wrong one and the compiler says which to pick: DurableClock in a render block is refused (it would
freeze at first paint), and outside a durable execution it fails loudly rather than quietly reading wall time.
⚠ The boundary is WHEN THE CODE RUNS, not which body it is written in. Pulling a render expression out into a
well-named method does not move it off the render path — it still runs on every pass, so it still wants DateTime.UtcNow,
and a DateTime.UtcNow left behind there freezes the page exactly as it would have in the slot:
bool IsDue(Plant p, DateTime now) => p.LastWateredAt.AddDays(p.EveryDays) <= now; // the instant is passed IN
// ⛔ NOT `bool IsDue(Plant p) => … <= DateTime.UtcNow;` — a clock read INSIDE a helper does not advance, and the
// compiler refuses it from a `live var`: "Reactivity is decided where the read is WRITTEN, not where the
// function is called." Read it at the call site — `items.Where(p => IsDue(p, DateTime.UtcNow))`.
action Water(Plant p) { p.LastWateredAt = DateTime.UtcNow; } // runs once per press → the one-shot readosy lint follows the calls, so it flags the first form written with DateTime.UtcNow and stays silent on a method
only an action reaches.
⚠ DateTime.Now is LOCAL time, and local is the APP's zone — exactly C#'s meaning, with the app standing where C#
has the machine. An app says which zone that is once, app.DefaultTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Europe/Stockholm");,
and DateTime.Now, DateTime.Today, DateTimeOffset.Now, d.ToLocalTime() and TimeZoneInfo.Local all read in it —
on the server, in the browser and in a query alike. It is never the HOST's zone: a server runs in UTC wherever it is
deployed and a browser in whatever its machine is set to, so a host reading would make one app answer differently by
where it ran. An app that declares no zone reads its organization's time zone when deployed on Osyrin (the organization's
"Time zone" setting, from the next deploy), and UTC anywhere else — so locally Now and UtcNow are the same instant. For
somebody else's wall clock, name their zone: TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, zone) (Time zones — TimeZoneInfo).
On screen: the clock that keeps moving#
A reactive context — a live var or a render slot — subscribes to the clock, so a value derived from it counts down
on screen with no refetch, no polling and no code beyond the expression:
live var left = deadline - DateTime.UtcNow; // ✓ advances
var openedAt = DateTime.UtcNow; // ✓ reads once — the instant the page mountedEvery reader on a page ticks together — one clock per page, not one per row — so two countdowns never disagree about what second it is. The cadence is one second; that is a display rate, not a precision claim. A page that never reads the clock starts no timer and costs nothing.
⚠ On screen it is a DISPLAY clock, never an authority. It is the browser's wall clock, so a viewer whose machine is skewed sees a skewed countdown:
- Nothing may be decided on it. Whether an SLA breached, whether an offer expired, whether a token is still valid — those are settled on the server against the server's clock and arrive as data. Render that answer.
- The honest division: the server decides what is true; the display clock animates an interval whose endpoint is already known.
Signature#
DateTime.UtcNow // the current instant (UTC)
DateTime.Now // the wall clock in the app's zone (app.DefaultTimeZone; UTC when none)
DateTime.Today // that wall clock's date, at midnight
TimeZoneInfo.Local // the app's zone itself
d.ToLocalTime() // an instant on the app's wall clock
DurableClock.Now / .UtcNow / .Today // inside a workflow/durable body: the ENGINE's instant
DateTimeOffset.UtcNow // the instant, at offset zero
DateTimeOffset.Now // the instant, at the app's zone's offsetDescription#
The platform owns "now". Because a durable function can suspend and resume, a naive system clock would return a different value on resume and corrupt replay — so every current-time read goes through the platform clock, which returns the same instant on replay of a statement that suspended and resumed. (A body re-entered from the top derives its instants afresh, as it re-runs everything not recorded in a step — see above.) The same mechanism makes time-reading logic time-travel-testable: a test can fix the clock and assert behavior at a chosen moment.
The platform is UTC-internal: a stored or computed DateTime is a UTC instant, because 9am in Frankfurt and
5pm in Tokyo are the same instant and that instant is what the value holds. Local time is the app's zone — what
app.DefaultTimeZone declares, UTC when it declares none — and DateTime.Now is that zone's wall clock, written as the
conversion it is (TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, TimeZoneInfo.Local)), so it ticks on screen, follows
a pinned test clock, and means the same in a query as in memory. DateTime.Today is its date.
Somebody else's local time is a question about that person — so it is asked for by naming their zone, in one line:
var instant = DateTime.UtcNow; // the fact
var local = TimeZoneInfo.ConvertTimeFromUtc(instant, TimeZoneInfo.FindSystemTimeZoneById("Europe/Stockholm")); // …read as somebody's wall clockStored and computed DateTimes stay UTC; the zone belongs at the display edge, where you know whose it is.
In a query they read the database's clock — or a test's pinned one — and DateTime.Now converts it to the app's zone
there, so Slot.Where(s => s.At <= DateTime.Now) counts the rows the same comparison counts in memory.
Parsing and formatting. DateTime.Parse(s) turns a string into a DateTime (and DateOnly.Parse /
TimeOnly.Parse / TimeSpan.Parse for the others); each throws on an unparseable string. value.ToString("fmt")
formats a date/time/duration with a standard .NET/C# format string (the same engine as a $"{d:fmt}" interpolation
hole) — e.g. d.ToString("yyyy-MM-dd"). Both are in-memory.
Examples#
entity Ticket {
[Required, MaxLength(200)] string Title;
DateTime DueAt;
[CreatedAt] DateTime CreatedAt; // the platform writes the instant the row was created
}
Ticket Open(string title) {
return new Ticket { Title = title, DueAt = DateTime.UtcNow + TimeSpan.FromDays(7) }; // faithful C# — the platform clock
}
bool IsOverdue(Ticket t) {
return DateTime.UtcNow > t.DueAt;
}
bool OpenedRecently(Ticket t) {
// `CreatedAt` is `[CreatedAt]` above: declared once, written by the platform, never assigned here.
return DateTime.UtcNow - t.CreatedAt < TimeSpan.FromHours(1); // TimeSpan arithmetic — see [TimeSpan (durations)](/reference/types/timespan/)
}Local time is the app's zone — declared once, then DateTime.Now and DateTime.Today read in it, in a query too:
app.DefaultTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Europe/Stockholm");
entity Lesson {
[Required, MaxLength(120)] string Title;
DateTime StartsAt;
security { allow read when IsAuthenticated; }
}
// Lessons starting later today on the school's own wall clock — the comparison runs in the database.
int LeftToday() {
var endOfDay = TimeZoneInfo.ConvertTimeToUtc(DateTime.Today.AddDays(1), TimeZoneInfo.Local);
return Lesson.Where(l => l.StartsAt >= DateTime.UtcNow && l.StartsAt < endOfDay).Count();
}
string ClockOnTheWall() { return DateTime.Now.ToString("HH:mm"); }See also#
- Time zones — TimeZoneInfo —
TimeZoneInfo: other people's zones, offsets, daylight saving - TimeSpan (durations) — durations, and
DateTime/TimeSpanarithmetic