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.md
  • tr1-select-articles-rotation-angles.md
  • my-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 here

You 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:

PropertyPurpose
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 documentation

If 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

  1. Template reads all {workflow}-rotation-*.md files
  2. Parses items from each file
  3. Computes day-of-year + delta
  4. Picks item via (doy + delta) % itemCount for each list
  5. Resolves cross-list [placeholders]
  6. 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:

FileScopeWhen to use
{workflow}-phrases.mdMasterPhrases available to every instantiation
{workflow}-{dateStamp}-phrases.mdInstancePhrases 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 specifically

Sub-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

VariableWhat it is
context.rotations["list-name"]Selected item object (.title, .description, .avoids, .notes, plus any custom)
context.rotationContextPre-formatted text block with day + mega + angle
context.phrasePicked phrase for this step (generic pool)
context.phrases["Goal"]Picked phrase from Goal sub-heading
context.phrases["CategoryName"]Picked phrase from any sub-heading
context.stepTitleTitle of the current step
deltaDay offset passed at instantiation
doyDay-of-year number used for rotation selection
author.nameAuthor name from author-config
targetDateThe target date string (yyyy-mm-dd)
creationDateWhen the file was created

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 doWhere to do it
Add/remove rotation itemsEdit {workflow}-rotation-{list}.md — add/remove ## headings
Add/remove phrasesEdit {workflow}-phrases.md — add/remove - bullets
Add a phrase categoryAdd a ### CategoryName heading under a step in the phrase file
Scope phrases to one stepPut bullets under ## Step 01 (or whichever step)
Scope phrases to one trackPut bullets under ## Step 02a (specific track)
Add phrases for one run onlyEdit the instance phrase file ({workflow}-{dateStamp}-phrases.md)
Cross-reference rotation listsUse [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

FileLocationLoaded by
rotation-resolver.jsscripts/workflow/Templater
phrase-lookup.jsscripts/workflow/Templater
date-numbers.jsscripts/workflow/Templater
dependency-checker.jsscripts/workflow/Templater
author-config.jsscripts/workflow/Templater
workflow-creator.jsscripts/workflow/Templater (also used by tests)
*.test.jstests/Node.js only
Rotation markdownAnywhere in vaultStep template scans by filename
Phrase markdownAnywhere in vaultStep template scans by filename