syntaxsugar

Wordflow Temporal Course · Reference

Wordflow contract

Every language branch implements the same HTTP API and uses the same Temporal names. The web app in web/ only knows about the HTTP API.

HTTP API

The routing, JSON encoding, and error mapping are provided. You implement the part of each endpoint that calls Temporal.

EndpointBodyReturnsLesson
GET /api/puzzles[{id, title, words, timeLimitSeconds}]provided
POST /api/games{puzzleId, player?}{gameId, game?}1 (guest), 7 (player)
GET /api/games/{id}Game view3
POST /api/games/{id}/guesses{word}{word, outcome, game}, or 202 with no body3 (202), 4 (200)
POST /api/games/{id}/extendGame view5
POST /api/players{name}Player view6
GET /api/players/{name}Player view6
POST /api/games/{id}/hints{pointsSpent, game}8
GET /api/leaderboard{entries: [{rank, name, totalEarned, gamesWon}]}9

Every POST from the web app sends an Idempotency-Key header. The API passes it to your code as the request ID.

An endpoint that is not built yet returns 501 {"error", "lesson"}, and the web app shows “This arrives in Lesson N”.

Error mapping

Temporal errorHTTP
Application error (a rejected Update, a failed Workflow)400 {"error", "code"}
Workflow not found404
Workflow already started409
Temporal unavailable503
Timed out waiting for a reply504 (usually: the Worker is not running)

Game view

{
  "gameId": "game-alice-1", "playerId": "alice", "puzzleId": "signal", "title": "Signal",
  "letters": "LANGIS", "status": "playing",
  "words": [{"length": 4, "pattern": "S___", "found": false}],
  "foundCount": 0, "totalWords": 9,
  "rejected": ["GLIN"], "bonusWords": ["SAIL"],
  "guesses": 3, "incorrect": 1,
  "freeHints": 2, "hintCost": 0, "extensionsLeft": 2,
  "expiresAt": "2026-10-04T18:00:00Z", "score": 0
}

status is loading, playing, won, or timed_out. score is set when the game ends.

Guess outcome is found, already_found, not_in_puzzle, bonus, invalid, timed_out, or game_over.

Player view

{"name": "alice", "joinedAt": "2026-10-04T17:55:00Z", "points": 20, "totalEarned": 0,
 "gamesPlayed": 0, "gamesWon": 0, "activeGameId": "game-alice-1"}

Temporal names

Use these exact names in every language, so the Temporal UI looks the same and the lessons read the same.

Task Queue: wordflow

Workflow typeWorkflow IDMessages
GameWorkflowgame-<requestId> (guest), game-<player>-<n> (child of a player)Query state, Updates ready, guess (a Signal in Lesson 3), extendTime, hint
PlayerWorkflowplayer-<name>Query player, Updates join, startGame, spendPoints
LeaderboardWorkflowleaderboardQuery leaderboard, Signal score

Optional exercises add TransferPointsWorkflow (Updates withdraw, deposit on PlayerWorkflow), PuzzleSubmissionWorkflow (ID submission-<puzzleId>, Signal review, Query submission), and DailyPuzzleWorkflow (Schedule ID daily-puzzle).

External services

The server also runs fake external services. Use them from Activities, never from Workflows.

EndpointPurpose
GET /services/dictionary/words/{word}200 real word, 404 not a word. Add ?region=NAME for 0–1.5 s of random latency.
GET / PUT /services/dictionary/modeok, flaky, slow, rate_limited, down, unauthorized
GET / POST /services/feedActivity feed. POST {"message"}

Game rules (provided)

Each language branch provides the game rules as plain, deterministic code: the game state, player state, leaderboard, and puzzle store. Lessons call that code; they never re-implement it.

  • Words are 3+ letters, made from the puzzle’s letters.
  • A wrong guess costs 1 point. A real word that is not in the puzzle is a bonus worth 2.
  • Score = letters in found words + bonuses − wrong guesses (+10 for finding every word), never below 0.
  • 2 free hints, then 10 points each. Guests cannot buy hints.
  • Up to 2 extensions of 30 seconds.
  • New players start with 20 points. Finishing a game adds its score.

Edit this lesson on GitHub