Workflow Lookup System — How-To Guide
This is a reference for the rotation and phrase lookup system used across all workflows. Everything here is configured through plain markdown files — no coding required.
The Two Selection Modes
There are exactly two ways the system picks items from a list.
1. Rotation (Modulo)
The system counts your items, takes a number (usually the day-of-year), and picks the item at position (number + delta) % itemCount. This cycles through every item exactly once before repeating.
Day 48, delta 1, 20 items → position (48 + 1) % 20 = position 9 → item #10.
This is the same logic the old doySentence newspaper system uses. The number defaults to day-of-year but can be any integer. Delta shifts the pick forward or backward.
Use rotation when: you want deterministic, guaranteed coverage of all items over time.
2. Random
The system picks a random item from the pool each time. No memory, no guarantee of cycling through everything.
Use random when: you want spontaneity and don’t care about even coverage.
Random is the default. If you don’t specify a mode, you get random.
Rotation Lookups (Mega-Categories, Angles, etc.)
Where files live
Rotation lists are plain markdown files in your vault. The naming pattern is:
{workflow-name}-rotation-{list-name}.md
Examples:
tr1-select-articles-rotation-mega-categories.mdtr1-select-articles-rotation-angles.mdmy-workflow-rotation-topics.md
The step templates scan for files matching this pattern automatically. Location in the vault doesn’t matter — only the filename.
File format
Each item is an ## heading with properties below it:
## Breaking News
description: High-velocity consumption of factual reporting
avoids: Users complain about doomscrolling
notes: Consider time-sensitivity
## Audio Journalism
description: Native podcast content
avoids: Podcast discovery challenges
notes: Growing market
custom-field: Any key you want hereYou can have as many items as you want. Add more items and the rotation cycle gets longer automatically. Remove items and it gets shorter. No code changes needed.
Properties
Every item gets title (from the heading) automatically. Everything else is optional:
| Property | Purpose |
|---|---|
description: | What this item is about |
avoids: | What to steer away from |
notes: | Extra context or tips |
{anything}: | Any custom key-value pair you want |
Cross-list placeholders
In any property value, use [list-name] to insert the selected title from another rotation list:
## Process Analysis
description: Apply [mega-categories] through an operational lens
avoids: Dry process documentationIf today’s mega-category selection is “Audio Journalism”, this resolves to:
Apply Audio Journalism through an operational lens
This works across any rotation lists in the same workflow.
How selection works at instantiation
- Template reads all
{workflow}-rotation-*.mdfiles - Parses items from each file
- Computes day-of-year + delta
- Picks item via
(doy + delta) % itemCountfor each list - Resolves cross-list
[placeholders] - Stores results in
context.rotations
Rotations always use modulo selection. This is the same behavior as the original newspaper doySentence system.
Phrases
Where files live
Two phrase files per workflow:
| File | Scope | When to use |
|---|---|---|
{workflow}-phrases.md | Master | Phrases available to every instantiation |
{workflow}-{dateStamp}-phrases.md | Instance | Phrases for a specific dated run only |
The cascade
Phrases cascade from broad to narrow. When a step instantiates, it builds a pool from all matching scopes:
Workflow scope (broadest)
└── Instance scope
└── Base step scope (e.g., Step 02 for all tracks)
└── Exact step scope (e.g., Step 02c for track c only)
All matching phrases from all levels get merged into one pool, then one is picked.
File format
---
workflow: 'my-workflow'
type: phrase-master
---
# My Workflow — Phrases (Master)
## Workflow
- This phrase is available to every step in every instantiation
- So is this one
## Step 01
- This phrase is only available to Step 01
- And this one
## Step 02
- Available to Step 02 and all its tracks (02a, 02b, etc.)
## Step 02a
- Only available to track 02a specificallySub-headed phrase categories
Add ### sub-headings under any step to create named categories:
## Step 01
- generic phrase (goes into context.phrase)
- another generic one
### Goal
- a goal-specific phrase (goes into context.phrases["Goal"])
- another goal phrase
### Motivation
- a motivation phrase (goes into context.phrases["Motivation"])Generic phrases (directly under ## Step 01, no sub-heading) go into context.phrase.
Sub-headed phrases go into context.phrases["CategoryName"]. They also include the generic phrases in their pool — so the Goal pool contains both goal phrases AND the generic ones.
You can have as many categories as you want. They’re just ### headings.
Selection mode for phrases
By default, phrases use random selection. To use rotation (modulo) instead, set phraseMode: 'rotate' when generating the workflow:
generateWorkflow('my-workflow', {
displayName: 'My Workflow',
dateStamp: '2026-02-17',
phraseMode: 'rotate', // ← this switches from random to modulo
steps: [...]
});This is set per-workflow at generation time. Your existing workflows keep random unless you regenerate with this option.
Using Lookups in Templates
In the YAML block
Between the CUSTOM YAML markers in any step template:
# CUSTOM YAML - Add your own YAML fields below this line
mega: '<% yamlStr((context.rotations["mega-categories"] || {}).title) %>'
angle: '<% yamlStr((context.rotations["angles"] || {}).title) %>'
phrase: '<% yamlStr(context.phrase) %>'
goal-phrase: '<% yamlStr(context.phrases["Goal"] || "") %>'
# CUSTOM YAML - End (add above this line)In the body
Between the SECTION markers:
<% context.rotationContext || "" %>
This outputs the full formatted rotation block (day, mega, angle, topic).
Or cherry-pick individual fields:
<% (context.rotations["mega-categories"] || {}).title || "N/A" %>
<% (context.rotations["mega-categories"] || {}).description || "" %>
<% (context.rotations["mega-categories"] || {}).avoids || "" %>
<% (context.rotations["angles"] || {}).title || "N/A" %>
Sub-headed phrases in the body
> *<% context.phrase %>*
**Goal:** <% context.phrases["Goal"] || "" %>
**Motivation:** <% context.phrases["Motivation"] || "" %>
The || {} guard
Always use (context.rotations["name"] || {}).title instead of context.rotations["name"].title. The guard prevents a crash when the rotation file doesn’t exist or hasn’t loaded yet. Same pattern for phrases: context.phrases["Goal"] || "".
All Available Template Variables
Configuring Without Code
Everything above is configured through markdown files and YAML. No JavaScript editing required for day-to-day use.
| What you want to do | Where to do it |
|---|---|
| Add/remove rotation items | Edit {workflow}-rotation-{list}.md — add/remove ## headings |
| Add/remove phrases | Edit {workflow}-phrases.md — add/remove - bullets |
| Add a phrase category | Add a ### CategoryName heading under a step in the phrase file |
| Scope phrases to one step | Put bullets under ## Step 01 (or whichever step) |
| Scope phrases to one track | Put bullets under ## Step 02a (specific track) |
| Add phrases for one run only | Edit the instance phrase file ({workflow}-{dateStamp}-phrases.md) |
| Cross-reference rotation lists | Use [list-name] in any rotation property value |
Architecture
How it connects to the old newspaper system
The old doySentence.js used hardcoded arrays and computed dayOfYear % arrayLength to pick an item. The new system does the same math but reads the arrays from markdown files instead of JavaScript. The modulo operation is identical.
doySentence.getSelectedMegaItem(delta) → context.rotations["mega-categories"]
doySentence.getSelectedAngleItem(delta) → context.rotations["angles"]
Both use (dayOfYear + delta) % count. Same result, different source.
Module responsibilities
rotation-resolver.js Parses rotation markdown → items, selects by modulo
phrase-lookup.js Parses phrase markdown → scoped pools, selects random or modulo
date-numbers.js Day-of-year calculation utility
dependency-checker.js Validates required modules are loaded at startup
author-config.js Author name/email for template YAML
workflow-creator.js Generates all template files from config (lives in scripts/workflow/)
Data flow at instantiation
Templater triggers step template
├── phrase-lookup reads {workflow}-phrases.md
│ ├── parsePhraseFile() → { workflow: [...], steps: { '01': [...], '01:Goal': [...] } }
│ ├── resolvePhrases('01', files) → generic pool
│ ├── resolvePhrases('01', files, 'Goal') → goal pool
│ └── pickRandom(pool) or pickByRotation(pool, doy, delta)
│
├── rotation-resolver reads {workflow}-rotation-*.md files
│ ├── parseRotationList() → [ { title, description, avoids, ... }, ... ]
│ ├── selectItem(items, doy, delta) → one item via modulo
│ └── resolveTemplate(item, replacements) → cross-list [placeholder] resolution
│
└── Template renders with context.phrase, context.phrases, context.rotations
The cascade in detail
For a step with ID 02c and a phrase file with these headings:
## Workflow → always included
## Instance → always included (instance file only)
## Step 02 → included because 02c is a track of 02
## Step 02c → included because exact match
## Step 02a → NOT included (different track)
## Step 03 → NOT included (different step)
Sub-headed categories follow the same logic:
### Goal under ## Step 02 → included in context.phrases["Goal"] for 02c
### Goal under ## Step 02c → included in context.phrases["Goal"] for 02c
### Goal under ## Step 02a → NOT included (wrong track)
File naming conventions
All files use kebab-case with the workflow name as prefix:
{workflow}-rotation-{list-name}.md Rotation lookup lists
{workflow}-phrases.md Master phrase file
{workflow}-{dateStamp}-phrases.md Instance phrase file
{workflow}-{dateStamp}-workflow.md Workflow runner template
{workflow}-{dateStamp}-home.md Dashboard/home template
{workflow}-{dateStamp}-{stepId}.md Step templates
What lives where
| File | Location | Loaded by |
|---|---|---|
rotation-resolver.js | scripts/workflow/ | Templater |
phrase-lookup.js | scripts/workflow/ | Templater |
date-numbers.js | scripts/workflow/ | Templater |
dependency-checker.js | scripts/workflow/ | Templater |
author-config.js | scripts/workflow/ | Templater |
workflow-creator.js | scripts/workflow/ | Templater (also used by tests) |
*.test.js | tests/ | Node.js only |
| Rotation markdown | Anywhere in vault | Step template scans by filename |
| Phrase markdown | Anywhere in vault | Step template scans by filename |