← Tidelines/Best practices

Your agent works nights. Nobody else does.

The chat widget goes 24/7 on Monday. The claims team still goes home at eight. Every rule in the prompt that ends in a person is now a promise about somebody who is asleep.

by TypeGlish team8 min read#best-practices
The desk shuts at eight. The chat does not.

TL;DR When an agent goes 24/7 and the people behind it do not, make staffing a runtime input instead of a phrase in a rule, put each shift's handoff and callback promise in its own $IF arm, keep the emergency route outside both arms, and build and test the night arm on its own.

Fernhill Pet Insurance runs a chat assistant called Moss. Until now the widget disappeared at 8pm, when the claims desk logged off. Next week it stays up all night, because most claims are filed by people sitting beside a sick animal at 11pm. The prompt was written for a building with people in it. Here are the eight checks for the pull request that sends it into an empty one.

§1Find the people in the prompt

moss.tg - the daytime prompt✓ A (100/100)
$IMPORT tool transfer_to_human

# Role
@@ role: one desk, one name, so every later "you" is the agent
You are Moss, the chat assistant for Fernhill Pet Insurance.

# Constraints
@@ human: a customer who asks for a person gets one
- WHEN the customer asks for a human THEN MUST call @[transfer_to_human].
@@ claims: claim decisions belong to the claims team
- WHEN the customer disputes a claim decision THEN MUST call @[transfer_to_human].
@@ callback: the desk calls back inside two hours
- MUST promise a callback within 2 hours.
@@ brevity: three sentences keeps a chat reply scannable
- MUST keep every reply to at most 3 sentences.
A tidy prompt. Every rule reads, every line has a reason, and three of the four rules assume a person is at a desk.
tg review - moss.tg, daytime✓ clean
$ typeglish review moss.tg
moss.tg - A (100/100)  check: 0 error, 0 warning, 0 info  tiers: base+z3

  FIXES  none - every facet is essentially full

  RULES  4 of 4 rule-shaped lines read

  TESTS  no $TEST cases - add one beside a rule; `test --dry` validates it offline

Check 1: list every rule that ends in a human. Read the constraints for the ones whose last step is a person: a transfer, a callback, a the team will. Here there are three. The transfer on request and the transfer on a disputed claim both call a tool whose other end is a queue, and the callback rule promises two hours on behalf of people who will not be back for twelve. None of these is wrong in the daytime. All three are wrong at 3am, and none of them will say so.

Check 2: the ban you add first is the one nothing checks. The obvious patch is two lines at the bottom: a looser callback promise for the night, and a ban on transferring when the desk is shut.

moss.tg - the night patch, as first written✗ logic/time-strength
$IMPORT tool transfer_to_human

# Role
@@ role: one desk, one name, so every later "you" is the agent
You are Moss, the chat assistant for Fernhill Pet Insurance.

# Constraints
@@ human: a customer who asks for a person gets one
- WHEN the customer asks for a human THEN MUST call @[transfer_to_human].
@@ claims: claim decisions belong to the claims team
- WHEN the customer disputes a claim decision THEN MUST call @[transfer_to_human].
@@ callback: the desk calls back inside two hours
- MUST promise a callback within 2 hours.
@@ brevity: three sentences keeps a chat reply scannable
- MUST keep every reply to at most 3 sentences.
@@ night: added for the 24/7 launch, the desk is shut from 8pm to 8am
- WHEN the desk is closed THEN MUST promise a callback within 12 hours.
@@ no_transfer: nobody is on the other end of the transfer at night
- WHEN the desk is closed THEN NEVER call @[transfer_to_human].
Line 19 is the one that matters: it is the difference between a customer waiting in an empty queue and a customer being told the truth.
tg check + score - the night patch✗ F (79/100)
$ typeglish check moss.tg
moss.tg:13:1  error  logic/time-strength  Conflicts with line 17. One bound per slot - "within 2 hours" already entails "within 12 hours"; the looser rule on "you promise a · callback" is dead weight. Keep one.
         ↳ line 17: - WHEN the desk is closed THEN MUST promise a callback within 12 hours.
moss.tg:17:1  error  logic/time-strength  Conflicts with line 13. One bound per slot - "within 2 hours" already entails "within 12 hours"; the looser rule on "you promise a · callback" is dead weight. Keep one.
         ↳ line 13: - MUST promise a callback within 2 hours.
moss.tg:17:1  info   prompt/agentless-passive  The doer is deleted (agentless passive) - who performs this? Name the actor or use the imperative: "Verify the order." instead of "The order is verified."
moss.tg:19:1  info   prompt/agentless-passive  The doer is deleted (agentless passive) - who performs this? Name the actor or use the imperative: "Verify the order." instead of "The order is verified."

✗ 1 file - 2 error, 0 warning, 2 info

$ typeglish score moss.tg | head -1
moss.tg - F (79/100)  proven errors - grade capped at F  tiers: base+z3
Two errors, both about the callback. Nothing about the transfer.

Read what the checker did not say. Line 9 says when the customer asks for a human, call the transfer. Line 19 says when the desk is closed, never call the transfer. At 11pm a customer asks for a human, and both triggers hold at once. The checker does not flag the pair, because the two rules hang off two different conditions and nothing in the file says they can be true on the same turn. The model will have to decide which rule wins, live, every night. That is the failure the patch was written to prevent, and it is still there.

Check 3: a guard does not shelter a promise from a global one. The callback pair is the part that blocks. MUST promise a callback within 2 hours has no guard, so it applies at night too, and a 12-hour promise under a WHEN is just a second, looser bound on the same slot: logic/time-strength, "within 2 hours" already entails "within 12 hours". The build is refused, which is the right result for the wrong reason. The fix is not to delete one of the numbers; both are true, on different shifts.

§2Make the shift an input

Check 4: whether anyone is on shift is a fact the runtime knows, not a thing the model infers. The desk is closed is a sentence the model has to judge from nothing: it draws prompt/agentless-passive twice in the output above, and it is a classifier with no evidence to classify, the problem every WHEN is a classifier takes apart. Nor should the prompt carry the hours and ask the model to compare them with a clock it does not have; why your agent gets the time zone wrong covers why. The platform already knows whether the claims queue has agents logged in. Pass that in: $REQUIRE variable desk: one of open, closed.

Check 5: each shift gets its own handoff and its own promise. With the shift declared, the rules that depend on it move into a $IF chain. The open arm keeps the transfers and the two-hour promise. The closed arm replaces each transfer with an act the agent can actually complete, and makes the promise the night team can keep.

moss.tg - one prompt, two shifts✓ A (99/100)
$REQUIRE variable desk: one of open, closed
$IMPORT tool transfer_to_human

# Role
@@ role: one desk, one name, so every later "you" is the agent
You are Moss, the chat assistant for Fernhill Pet Insurance.

# Context
@@ helpline: the one service that is staffed around the clock
Fernhill runs a vet helpline that answers 24 hours a day.

# Constraints
@@ handoff: the runtime says whether a person is on shift, so the prompt never guesses
$IF @{desk} is equal to open:
  - WHEN the customer asks for a human THEN MUST call @[transfer_to_human].
  - WHEN the customer disputes a claim decision THEN MUST call @[transfer_to_human].
  - MUST promise a callback within 2 hours.
$ELSE:
  - WHEN the customer asks for a human THEN MUST offer a callback.
  - WHEN the customer disputes a claim decision THEN MUST offer a callback.
  - MUST promise a callback within 12 hours.
@@ emergency: a sick animal does not wait for the morning shift
- WHEN the customer reports a pet emergency THEN MUST give the customer the vet helpline.
@@ brevity: three sentences keeps a chat reply scannable
- MUST keep every reply to at most 3 sentences.

$TEST human
  - input:: I want to talk to an actual person about my claim.
  - expect::
    - offers a callback
    - promises a callback within 12 hours
    - at most 3 sentences

$TEST emergency
  - input:: My dog just ate a pack of raisins, what do I do?
  - expect::
    - gives the customer the 24-hour vet helpline
The two callback numbers now live in arms that cannot both apply, so the logic/time-strength errors are gone without either number being deleted. And the transfer rule does not exist at night at all, so there is nothing for the model to referee.
tg review - moss.tg, two shifts✓ 8 of 8 read
$ typeglish review moss.tg
moss.tg - A (99/100)  check: 0 error, 0 warning, 0 info  tiers: base+z3

  FIXES  none - every facet is essentially full

  RULES  8 of 8 rule-shaped lines read · 8/8 covered by $TEST

  TESTS  2 cases · 8/8 rules exercised

Check 6: the emergency route belongs to neither shift. Some contacts cannot wait for the morning, and for a pet insurer that is a sick animal. That rule sits outside the chain, so it ships in both builds, and it points at something that is actually staffed around the clock: the vet helpline, introduced once in # Context so the rule has a definite thing to hand over. If your emergency route is a human queue that also closes at eight, you do not have an emergency route at night, and the prompt is the wrong place to fix that.

A handoff rule is a claim about a building. Check whether the lights are on.

§3Build the night, then test it

Check 7: build each shift and read the one nobody has watched. The daytime prompt has months of transcripts behind it. The night prompt has none. Build it with the variable bound and read exactly what the model will get at 3am:

tg build --vars - the night artifact✓ built
$ typeglish build moss.tg --vars '{"desk":"closed"}'
✓ built .typeglish/dist/moss.txt ← moss.tg (b2b457d15081, full)

$ cat .typeglish/dist/moss.txt
# Role
You are Moss, the chat assistant for Fernhill Pet Insurance.

# Context
Fernhill runs a vet helpline that answers 24 hours a day.

# Constraints
- WHEN the customer asks for a human THEN MUST offer a callback.
- WHEN the customer disputes a claim decision THEN MUST offer a callback.
- MUST promise a callback within 12 hours.
- WHEN the customer reports a pet emergency THEN MUST give the customer the vet helpline.
- MUST keep every reply to at most 3 sentences.
No transfer rule, no two-hour promise, no mention of a desk. The model cannot pick the wrong shift, because it only ever sees one.

And know what you get if the platform forgets to pass the variable:

tg build - no variable bound✗ both shifts ship
$ typeglish build moss.tg
✓ built .typeglish/dist/moss.txt ← moss.tg (627a21bab4ad, full)

$ cat .typeglish/dist/moss.txt
# Role
You are Moss, the chat assistant for Fernhill Pet Insurance.

# Context
Fernhill runs a vet helpline that answers 24 hours a day.

# Constraints
If desk is open:
  - WHEN the customer asks for a human THEN MUST call transfer_to_human.
  - WHEN the customer disputes a claim decision THEN MUST call transfer_to_human.
  - MUST promise a callback within 2 hours.
Otherwise:
  - WHEN the customer asks for a human THEN MUST offer a callback.
  - WHEN the customer disputes a claim decision THEN MUST offer a callback.
  - MUST promise a callback within 12 hours.
- WHEN the customer reports a pet emergency THEN MUST give the customer the vet helpline.
- MUST keep every reply to at most 3 sentences.
An unbound build ships both arms as conditional prose, and If desk is open is back to being a judgement the model makes from nothing. It is the same trap as the peak-season block pasted above your Role. Deploy the two bound artifacts, and have the runtime pick one per conversation.

Check 8: write the tests for the night. The daytime path has been exercised by real customers. The night path is new, and it is the one where the agent has fewer ways out, so that is where the cases go: a request for a person that must end in a callback with the night number, and an emergency that must end at the helpline.

tg test --dry - moss.tg✓ 8/8 rules exercised
$ typeglish test moss.tg --dry
✓ moss.tg  coverage: 8/8 rules exercised
  · human - "I want to talk to an actual person about my claim." (not run)
      ✓ at most 3 sentences
      · rubric - 2 prose expectations, judged on a live run (needs ANTHROPIC_API_KEY)
  · emergency - "My dog just ate a pack of raisins, what do I do?" (not run)
      · rubric - 1 prose expectation, judged on a live run (needs ANTHROPIC_API_KEY)
✓ 1 prompt - 0 failed
Read the coverage line honestly. A case covers a rule when its expectations share the rule's wording, so 8/8 means every rule has a case that talks about it, not that both arms were run. Run the live suite against the night artifact before launch; the at most 3 sentences assert already runs offline.

§4Common questions

How do I handle after-hours escalation in an AI support agent?
Make whether anyone is on shift a runtime input, for example $REQUIRE variable desk: one of open, closed, and put the escalation rules inside a $IF chain on it. The open arm transfers to a person; the closed arm offers a callback with a number the night team can keep. Build each arm with --vars and deploy both artifacts, so the model only ever sees the rules for the shift it is on.
Why does my chatbot try to transfer customers when nobody is available?
Because the transfer rule has no idea what time it is. A night-time ban written as WHEN the desk is closed THEN NEVER call the transfer sits beside WHEN the customer asks for a human THEN MUST call it, both can hold on the same turn, and the checker does not flag the pair. The model decides which one wins, live. Remove the choice by making the transfer rule exist only in the daytime build.
Should I put business hours in my agent's system prompt?
Not as something for the model to reason about. The model has no reliable clock and no idea whether the queue is staffed today; your platform does. Pass the staffing state in as a variable and let the build pick the rules, and keep any hours you show customers as plain facts in # Context.
Can one system prompt serve both day and night shifts?
One source file, yes; one artifact, no. Keep both shifts in one .tg file so they are reviewed and checked together, with different callback promises in different $IF arms, which is why two numbers that would be a logic/time-strength error side by side check clean. Then build each shift separately, because an unbound build ships both arms as conditional prose for the model to choose between.
Field note

Going 24/7 is usually sold as a coverage win, and it is one, but the rules that break are never the ones about answering questions. They are the ones about handing over, because a handover is a claim about the people behind the agent, and those people keep office hours. Teams find this out from the first overnight transcript where a customer waited forty minutes in a queue nobody was watching. The fix is small and it is mostly not prose: one variable, one chain, two builds, and a test case for the shift with the lights off. Do it in the same pull request that changes the widget's hours.

∿ washed up Sep 27, 2026 ∿