Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Reference / Enum

[Label], [Icon], [Tone] — what a human reads

[Label("<label>")] <Member>, /// <description> <Member>,

An enum member stores a compact value but shows a human-readable label. [Label("…")] gives a member its label (otherwise the member's own name is used), and a doc comment gives it a longer description. A screen that shows an enum-typed value — a grid cell, a text line — reads the label, never the stored value. [Icon(…)] and [Tone(…)] say which icon and which tone stand for a member, so every screen shows it the same way without repeating the decision.

stable4 examples compiled by CIenumuidisplaylabels

Summary#

An enum member has two faces. It has a stored value — the compact thing that lives in the column and that your code compares against — and it has a label, the words a person reads on screen. [Label("…")] sets the label. Without one, the label is simply the member's name.

You never have to convert between them. Show an enum-typed value anywhere in a screen and the label is what appears.

Signature#

enum <Name> {
  [Label("<label>")] <Member>,     // an explicit label

  /// <description>
  <Member>,                          // a doc comment becomes the member's description
}

Description#

How do I change the words a member shows?#

A member name is an identifier, so it cannot contain spaces or punctuation. [Label] supplies the words:

enum OrganizationType {
  /// A single person's own space.
  Personal,

  [Label("Team or company")] Team,
}

entity Organization {
  [Required, MaxLength(100)] string Name;
  OrganizationType Type = OrganizationType.Team;
}

Team reads as Team or company. Personal declares no [Label], so it reads as Personal — the member name is a perfectly good label when it already says what it means, and you should not add [Label("Personal")] just to be explicit.

A doc comment on a member becomes its description — a longer sentence a control can show beside the label, such as the help text under an option in a dropdown. It is optional; a member with no doc comment simply has none.

How do I show an enum value on a screen?#

Anywhere a screen displays an enum-typed value, it displays the label:

[Page("/orgs")]
[Render(CSR)]
component OrganizationsPage() {
  var orgs = Organization.ToList();

  render {
    Stack(gap: 2) {
      foreach (var o in orgs) {
        Row(gap: 3) {
          Text(o.Name);
          Text(o.Type);        // "Team or company" — the label, not the stored value
        }
      }
    }
  }
}

A grid column reads the same way: give it the member's key (Type) and the cell shows the label. You never write a lookup, a mapping, or a switch to turn a value into words.

Because the label is resolved only for display, the value itself is untouched — o.Type == OrganizationType.Team still compares against the stored value, exactly as it always did.

When you need the label as a STRING — .Label#

Displaying a value shows its label without being asked. But some props take a string, not a value to render — a Button's label, a title, an aria name — and there the label has to be read. .Label is that read.

The difference is the SLOT, not the value — one table so it never has to be worked out:

you writeyou getwhy
Text(dish.Cuisine)Street food — the labela text slot RENDERS an enum value
Badge(dish.Cuisine.Label, …)Street fooda string PARAMETER takes a string, so read the label
Text("in " + dish.Cuisine)in StreetFood — the member name+ is a string op, and an enum's string form is its name (C#-exact)
Text("in " + dish.Cuisine.Label)in Street food…so say .Label when you concatenate
Row { Text("in "); Text(dish.Cuisine); }in Street food…or give the enum its own text slot

⚑ The third row is the one that surprises, and it is not a compile error — it renders, just with the wrong words. Two generated apps in a row reasoned their way to the last row from first principles rather than reading it here.

enum Cuisine { [Label("Street food")] StreetFood, Thai, Nordic }

[Page("/cuisines")]
[AllowAnonymous]
component CuisineFilter() {
  Cuisine picked = Cuisine.Thai;
  action Pick(Cuisine c) { picked = c; }

  render {
    Row(gap: 2) {
      foreach (var c in Cuisine.Members) {
        Button(c.Label, onPress: () => Pick(c));   // "Street food", not "StreetFood"
      }
    }
  }
}

Cuisine.Members is every member of the enum, so a picker is a foreach and stays right when a member is added. .Description, .Name, .Icon and .Tone read the rest of a member's presentation the same way — .Name is the member's identifier ("StreetFood"), which is what .ToString() gives you and almost never what a person should read.

⚠ They are properties, not methods: r.Label, never r.Label().

The stored string must differ from the name — [Value]#

Under [Type(string)] an enum member is stored by its name by default. When the stored string must differ from the name — to match an external system, or to keep a stable code while the member is renamed — the [Value] attribute sets it explicitly: [Value("team")] Team stores "team" while your code still writes OrganizationType.Team. Most enums never need it; reach for it only when the storage string is a contract with something outside your app.

Which icon and tone stand for a member? — [Icon] and [Tone]#

A label is not the only thing a member has. "Cancelled" is usually also a cross, and usually also a danger — and those are facts about the member, not about the screen that happens to be showing it. Say them once:

enum Tone { Neutral, Success, Warning, Danger, Accent }

enum OrderState {
  [Label("Active"),    Icon(check), Tone(Tone.Success)] Active,
  [Label("On hold"),   Icon(pause), Tone(Tone.Warning)] OnHold,
  [Label("Cancelled"), Icon(close), Tone(Tone.Danger)]  Cancelled,
}

Without this, every dropdown, cell, badge and header re-decides with its own if (state == Cancelled) chain — which is how the same enum ends up red on one screen and grey on the next.

[Icon] names a declared icon; [Tone] names a declared tone. Icon(check) must name an icon your app can draw — one it declares (an icons/check.svg) or one of the built-ins — written unquoted.

Here the name is BARE, and at a call site it is qualified. The attribute takes Icon(check); rendering the same glyph directly takes Icon(Icons.Check). The two spellings are not interchangeable, and writing the qualified form in the attribute is a compile error. Tone(Tone.Danger) is a qualified reference to a member of a tone enum — the same shape as [Classification(DataClass.PII)] — so the referenced member is checked. A typo in either is a compile error with a suggestion, not a blank space at runtime.

[Tone] names a tone and never a colour. [Tone(Tone.Success)] says the member is a success and lets the design system decide what that looks like — so re-theming the app carries every enum with it, and a dark mode does not need the enum edited. A hex here would put presentation in your domain model permanently.

Neither has a fallback. A member with no [Icon] has no icon, and a screen is free to show nothing — unlike the label, where something must always be shown.

Changing a label is safe; changing a name is not#

The label is presentation, so you can reword it freely — no stored data refers to it. The member name, by contrast, is what your code names (OrganizationType.Team), and under [Type(string)] it is also what the column stores (unless a [Value] pins it). Reword the label when the words are wrong; rename the member only when the concept is.

See also#

Related

enum

A fixed set of named values, used as a member type. Stored as a number by default, or as the member's own name with…

entity members

The typed members an entity holds — text, numbers, dates, booleans, Guids, enums and references. A member's type…

control — foreign UI controls (charts, grids, maps)

A `control` block declares the contract of a foreign UI widget — a chart, a data grid, a map — that a small JavaScript…