What content designers need to know about typed parts

Writing rules for copy we don't control: my first days in the engineers' schema

What content designers need to know about typed parts
Photo by Elizabeth George / Unsplash

My team was presenting a UX proposal to a group of technical colleagues.

While I was explaining the rationale behind my writing choices, the engineering lead said that some of the fields were meant to be dynamic so they could adapt to different use cases.

He referred to these as typed parts and said they’d be controlled by guidelines rather than static copy.

The next day, I asked him to share the parts we already had. The snippet I got looks something like this:

class Part(BaseModel):
    text: str | None = Field(default=None, description="Text part")
    code: str | None = Field(default=None, description="Code part")
    ...

Several fields followed. Much of what the model sends to users in a conversation passes through them, but it was hard for me to tell which part carried what.

I spent a few days working this out with help from the machine learning team, and these are the notes I wish I’d had at the start.


What typed parts are

When a model replies to a user, the backend sends the answer as a sequence of pieces. Each piece carries a label saying what kind of content it is: text, a code block, a chart, a file or a link, for example.

The type is a contract between backend and frontend. The backend guarantees which types can appear and what fields each one has, and the frontend decides how each is rendered.


Why they matter for content design

The list of typed parts is effectively the list of things we have to write for, and the part type tells us whether we need static copy or guidelines.

For a normal screen we write and deliver the strings. This is good old UX writing as we know it: loading states, error messages, placeholders, tooltips, labels, buttons and so on.

For the parts where the model generates the content, we don’t control the exact copy users will see. Those strings aren’t deterministic and will be different every time.

So we write guidelines instead: rules about how the copy should behave, with examples of what follows a rule and what breaks it.


What changes when copy becomes rules

Most guidelines can start as plain Markdown: the rule, the reason for it and examples.

### Empty results

If a search returns nothing, say so in one sentence and offer an
alternative. Don't apologize: the user needs a next step, not
reassurance.

- **Do:** No results for X. Want me to try Y?
- **Don't:** I'm sorry, unfortunately I wasn't able to find…

Rules like this one are broad. They cover tone, what to do when something fails and how much to explain. They mostly read like the documents we already maintain as content designers. Anyone on the team can review them and push back, which matters while the rules are still taking shape.

At some point, though, each rule will need a way to be checked. Without one there’s no way to tell whether a prompt change affected the output, and no way to hand the rules to an engineer who wants to test them.

This is the eval portion, and it’s where a more structured format helps. We pair our rules with a criterion something or someone can score:

- id: text-002
  rule: "If a search returns nothing, say so in one sentence and
         offer an alternative. Don't apologize."
  negative_example: "I'm sorry, unfortunately I wasn't able to find…"
  positive_example: "No results for X. Want me to try Y?"
  eval:
    criterion: "The response states that nothing was found, without
                apology language, and offers a next step."
    type: pass_fail
    judge: llm

Same rule as above, with the parts a script can read: an id, the two examples as separate fields and the criterion to score against.

There are three things we need to decide for each rule: what gets checked, how it’s scored and who scores it.

What gets checked is the criterion. It should describe an observable property of the output, narrow enough that two people reading the same response would reach the same verdict. A rule you can’t turn into a criterion is usually too vague for the model to follow.

How it’s scored comes down to three options:

  • Pass/fail is for anything that’s either there or not there. For example, if the rule says a confirmation message should anticipate the consequences of a destructive action for the user and that explanation isn’t there, that’s a fail.
  • A 1 to 5 scale suits judgment calls like tone or clarity, provided you write anchors for at least 1, 3 and 5 and say which score counts as passing.
  • The third option is a regex or a small script, for literal strings and formats. It’s the cheapest to run and the least arguable, so it’s worth using wherever a rule allows it.
- id: text-007
  rule: "Explain errors in terms of what the user can do next,
         not what the system did wrong internally."
  negative_example: "The upstream provider returned a 401."
  positive_example: "Your connection to Drive expired. Reconnect
                     to keep using it."
  eval:
    criterion: "How actionable is the error message for a
                non-technical user?"
    type: scale_1_5
    anchors:
      1: "Names internal components or HTTP codes, no action given."
      3: "Plain language, but the user has to infer the next step."
      5: "States the cause in user terms and names one clear action."
    judge: llm
    threshold: 4

Who scores it is either a person reviewing a sample of outputs, or another model prompted with your criterion. Model-as-judge is what makes it possible to check more than a handful, and it depends on how specific the criterion is.

Handing over a concrete rule set gives people something to correct, and the corrections are often where you learn the most.


Which format to write rules in

Should you always prefer YAML over Markdown, then? Not necessarily. In fact, not every rule you write needs to be that specific from the beginning.

If you only have general guidelines and commentary, Markdown should be fine as a starting point. Structured material, like individual questions and their feedback, can go in YAML, because single examples can then be pulled out programmatically.

A mixed setup also works: two files, one for the broader considerations and one for the specific examples.

It’s also a question for the team that will use your file. Whatever you pick will probably change once someone tries to test the rules.

If you’ve never collaborated with your machine learning engineers this way, talk to them. Together you’ll figure out how to start and what to adjust as you go.


What to ask your engineers

Most of what I’ve understood so far comes from questions. These are the ones that have been most useful for me:

  • Who owns the schema? If several teams can add types, we need to know who to reach out to and how to access it.
  • Can a part hold more than one kind of content at a time, or always exactly one? Fields are often all optional, which leaves it open. If more than one is possible, we need to know which takes precedence on screen.
  • What shows when a part fails, expires or arrives empty? Raw backend strings can still show up in testing environments if no one manages them.
  • Can each new type ship with a line saying what fills it and where it renders? A description is useful when it says something the type doesn’t. This way it’s easier to filter new parts as the list grows, and to understand what each one does.

Where to start if you’re unsure

Start with the parts you already recognize in the product, rather than the ones with the clearest definitions in the code. Understanding what fills a part and where it appears is what lets you write anything real about it. Your product knowledge will really influence how solid your rule set is.

For the rest, the question I’ve found most useful is who writes the content:

  • Does the model generate it? Then it needs rules and a way to check them.
  • Is it fixed? Then it’s UX copy you write and ship together with the design spec.
  • Does someone else write it? If your type carries a custom module or was built by someone external, you can’t write the content or score it. What you can do is set constraints: what the component must contain, maximum lengths and what renders when one arrives malformed or empty.

Still figuring this out

This is a work in progress for me! I still have open questions about how to write the rules, especially the YAML ones, which clients are involved and which teams I should ask.

I’ll probably write more about the topic as I continue learning. Stay tuned.

CTA Image

Ciao 👋 I’m Elisa, an Italian product writer and translator who believes good design is service. This is where I document my work in UX.

Go to the blog

Let’s talk words

Connect with me on LinkedIn or Medium to talk about all things content design, UX writing and localization.

Contact me