# [CreatedAt] [CreatedBy] [ModifiedAt] [ModifiedBy]

> Row stamps: an entity declares a field and marks it, and the platform writes into it when the row is saved — the instant it was created, who created it, the instant of the last write, who made it. An entity that declares none has none.

<!-- id: entity-row-stamps · area: entity · stability: stable · html: https://osysharp.com/reference/entity/row-stamps/ -->

## Summary        {#summary}
A **row stamp** is a field the platform writes for you when a row is saved. You declare the field, mark it with one of
four attributes, and never assign it again:

| Attribute | Type | Written |
|---|---|---|
| `[CreatedAt]` | `DateTime` | the instant the row's unit of work committed — once, when the row is created |
| `[CreatedBy]` | `Guid?` | the id of whoever was signed in — once, when the row is created |
| `[ModifiedAt]` | `DateTime` | the instant of the last commit that wrote the row, its creation included |
| `[ModifiedBy]` | `Guid?` | the id of whoever made that write |

Nothing is stamped that you did not declare. An entity with no stamp has no such column — the platform adds nothing to
a row behind your back.

## Signature      {#signature}
```osy syntax
entity Order {
  [CreatedAt]  DateTime Placed;      // the field's name is yours
  [CreatedBy]  Guid?    PlacedBy;    // a WHO stamp is a nullable Guid: a system write has nobody signed in
  [ModifiedAt] DateTime Changed;
  [ModifiedBy] Guid?    ChangedBy;
}
```

## Description    {#description}

### When does the platform write them?   {#when}
On the commit that saves the row. `[CreatedAt]` and `[CreatedBy]` are written once, when the row is created;
`[ModifiedAt]` and `[ModifiedBy]` on that commit and on every commit that changes the row after it. The instant is the
commit's own — the same for every row it writes, and the pinned clock in a test (see [TestClock.Advance and TestClock.Set](https://osysharp.com/reference/testing/clock-advance/)).

A write the system makes with nobody signed in — a scheduled job, a migration — writes the instant stamps and leaves the
`Guid?` ones as they were.

```osy title="an entity that keeps when, and by whom, each row was made" test app=entity-row-stamps
entity Sighting {
  [Required, MaxLength(200)] string Species;
  int Count;
  [CreatedAt] DateTime Logged;
  [CreatedBy] Guid? LoggedBy;
  [ModifiedAt] DateTime Changed;
}

void LogSighting(string species, int count) {
  new Sighting { Species = species, Count = count };   // no stamp is set here — the commit writes them
}

List<Sighting> Newest() {
  return Sighting.OrderByDescending(s => s.Logged).Take(20).ToList();
}

int LoggedToday() {
  var today = DateTime.UtcNow.Date;
  return Sighting.Where(s => s.Logged >= today).Count();
}
```

### Can my code write a stamp?   {#writing}
No — and the compiler says so rather than letting the value vanish. A stamp in an object initializer is refused,
because the commit would replace it; so is any assignment to a `[ModifiedAt]`/`[ModifiedBy]` field, which every
commit rewrites. To make a row *as* someone — a test world's rows, say — create it inside `runas (Ada) { … }`.

A `[CreatedAt]` assigned on a row that already exists is kept: that is how a test backdates something to see it age.

A client, an API call and an MCP tool cannot write a stamp either: a value sent for one is ignored, on every surface.

### Who owns a row?   {#ownership}
`[CreatedBy]` is the field an ownership rule reads, and it is safe to read because nobody can forge it — the platform
writes the signed-in person's id over whatever was sent:

```osy title="each person sees and edits only the notes they wrote" test app=entity-row-stamps-ownership
[Principal]
entity Member {
  [Required, MaxLength(200)] string Email;
  security { allow read where Id == user.Id; }
}

entity Note {
  [Required, MaxLength(200)] string Text;
  [CreatedBy] Guid? Author;
  security {
    allow read, update, delete where Author == user.Id;
    allow create when IsAuthenticated;
  }
}
```

### What if I name a field `CreatedAt` without the attribute?   {#plain-field}
Then it is an ordinary field: you set it, and the platform leaves it alone. That is the shape for a date that comes from
somewhere else — the moment an imported order was placed in the shop it came from, say.

Reading a stamp an entity does not declare is a compile error that names the line to add:

```text
entity 'Ticket' has no property 'CreatedAt' — the instant a row was created is not a column the platform adds
behind your back: declare it on 'Ticket' with `[CreatedAt] DateTime CreatedAt;`, and the platform writes it.
```

### Where can a stamp go?   {#placement}
On a stored entity's field of the right type: `DateTime` (or `DateTime?`) for an instant, `Guid?` for a person. One
field per stamp, one stamp per field. A `class` has no rows to stamp, and an external entity's rows are written by its
data source, so both refuse the attributes by name.

## See also {#see-also}
- [entity](https://osysharp.com/reference/entity/declaration/): what an entity is, and the `Id` every one has
- [security { }](https://osysharp.com/reference/security/entity-security/): rules that read a row's fields, `[CreatedBy]` among them
- [TestClock.Advance and TestClock.Set](https://osysharp.com/reference/testing/clock-advance/): a pinned clock is the instant a commit stamps
