Osy#betaRun it on Osyrin: osyrin.com

Reference / Function

Current time (DateTime.UtcNow, DurableClock.Now)

DateTime.UtcNow · DateTime.Now / DateTime.Today (the app's zone) · DurableClock.Now / .UtcNow / .Today · DateTimeOffset.UtcNow / .Now

Read the current instant. `DateTime.UtcNow` (and the platform-idiomatic `DurableClock.Now`/…) return the current time from the platform's replay-safe clock — so durable functions resume deterministically and any time-reading logic is time-travel-testable. `DateTime.Now` and `DateTime.Today` are C#'s local reads, and local is the APP's zone (`app.DefaultTimeZone`, UTC when none is declared) — never the host's.

stable2 examples compiled by CIfunctiondatetimeclocktime

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 wantwritewhere
the engine's instant, replay-stableDateTime.UtcNow, or DurableClock.UtcNow to insist on ita workflow or durable body
a one-shot reading of nowDateTime.UtcNowanywhere
a value that KEEPS MOVING on screenDateTime.UtcNowanything 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 read

osy 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 mounted

Every 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 offset

Description#

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 clock

Stored 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#

Related

TimeSpan (durations)

`TimeSpan` is the duration type — a length of time, as in C#. Build one with the `TimeSpan.FromX` factories or `new…