Skip to content

Port the Nexus workshop to TypeScript: code + Instruqt track - #13

Merged
nadvolod merged 22 commits into
mainfrom
typescript-nexus-challenge-01
Aug 31, 2026
Merged

Port the Nexus workshop to TypeScript: code + Instruqt track#13
nadvolod merged 22 commits into
mainfrom
typescript-nexus-challenge-01

Conversation

@nadvolod

@nadvolod nadvolod commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Adds typescript/ as a language sibling of java/ and kotlin/: the exercise and solution code, the Instruqt track, the sandbox image, the architecture diagram, and the CI caller.

The Deck lives in temporalio/temporal-devdays-ts (PR #1) per that repo's ADR-0001.

Track identity — read this first

track.yml has no id: and no checksum:, no assignment.md carries an id:, and no tab carries an id:. The slug is new: nexus-typescript-decouple-monolith.

That is deliberate. Copying the Kotlin ids would make the first publish target yopxoc2jf3jb — the Kotlin track — and _track-ci.yml uses --force on merge to main. Please confirm the Kotlin track is untouched afterwards.

What was verified

Check Result
instruqt track validate passes
All 24 tab-N button refs resolve to the right tab by label passes
Every /root/workshop path referenced by a lifecycle script exists 11/11
All lifecycle scripts bash -n passes
Diagram server: serves, 404s, rejects ../ traversal passes
tsc --noEmit on both trees, npm ci from clean passes
Monolith run end to end TXN-A COMPLETED/LOW · TXN-B COMPLETED/MEDIUM · TXN-C DECLINED/HIGH
Decoupled run end to end Nexus check → park → sync review → payment-TXN-B COMPLETED with CONF-TXN-B

The tab-N check is its own script rather than trust in validate-track.sh, which INSTRUQT.md documents as silently stopping that check after the first publish.

Two lessons change shape, and the assignments say so

Challenge 2 now ends with a deliberately failing typecheck. Declaring an Operation obliges a matching handler at compile time:

TS2345: Type '{}' is missing the following properties from type
'ServiceHandlerFor<...>': checkCompliance, submitReview

Kotlin fails at Worker startup with Missing handlers for service operations. TypeScript makes that class of mistake unable to reach a running process — a better version of the same lesson, but it means challenge 2 legitimately leaves the build red until challenge 3. The assignment says so in as many words, and 02/solve-workshop deliberately does not typecheck.

Challenge 3's proof of registration inverts. There is no Nexus Poller line to look for. Instead, a Worker without nexusServices logs No Nexus services registered, not polling for Nexus tasks, and a correctly wired one does not. Absence is the signal, so the assignment tells learners to go looking for it deliberately. Both states were observed live, not inferred.

Challenge 4 also drops the Kotlin claim that "the Endpoint name belongs on the Worker, not in the Workflow." In TypeScript createNexusServiceClient requires endpoint at the call site. That challenge has a section addressed to anyone who has taken the Java or Kotlin version.

Sandbox differences from kotlin/

  • node:22-bookworm, memory: 4096 rather than 8192 (two JVMs and a Gradle daemon needed the headroom; Node does not).
  • No language extension is installed, deliberately. The Kotlin image pinned a syntax-only extension to avoid a language server competing with two JVMs. Here the built-in TypeScript service is wanted — it surfaces the challenge-2 compile error as a live red squiggle.
  • jwebserver is gone with the JDK, so a ~40-line Node static server (sandbox/serve-diagrams.mjs) serves the diagram on 8090.
  • Track setup symlinks node_modules instead of copying it: 292 MB per tree, and copying both would move ~600 MB before the attendee can type. Consequence: npm install inside the lab cannot write to node_modules. The workshop never needs it.
  • The architecture diagram's node data was rewritten — 23 replacements, zero residual .kt / Gradle / JVM references.

Two bugs the code port only found by running it

  • proxyActivities silently types away synchronous activities. validatePayment and checkCompliance were sync; call sites failed with Type 'Symbol' has no call signatures — verbatim the error the SDK's own JSDoc quotes for its NotAnActivityMethod sentinel. All Activities are now async.
  • A setHandler validator taking no arguments forced Args = [] and selected the wrong overload.

The sandbox image is built and pinned

ghcr.io/nadvolod/edu-nexus-typescript-sandbox@sha256:ce2f1b9f1671a9f6b61f7042cc417e95ca887d09acbb62ec209ae5b7e540ea44, built --platform linux/amd64 and pushed. config.yml pins that digest, and instruqt track validate still passes with it.

Verified against the pushed image, not just the build:

Check Result
uname -m x86_64
node / npm v22.23.2 / 10.9.8
temporal CLI 1.7.2 (Server 1.31.1)
code-server, both user-data-dirs seeded present
workshop trees 13 .ts files, 98 node_modules entries each
diagram + serve-diagrams.mjs present
monolith run inside the image worker starts; TXN-A COMPLETED/LOW, TXN-B COMPLETED/MEDIUM, TXN-C DECLINED/HIGH

That last row is the one that mattered: it proves @temporalio's native core bridge resolved for linux/amd64 from a lockfile generated on darwin-arm64.

typescript/.dockerignore is load-bearing rather than tidiness — without it the build ships ~600 MB of host-built, wrong-architecture node_modules as context. Context drops from 584 MB to 0.3 MB.

Still not done

Nothing has been sent to Instruqt. track.yml has never been published, so the ids it will be assigned do not exist yet. The first publish is the moment to confirm the Kotlin track is untouched.

🤖 Generated with Claude Code

Adds typescript/ as a language sibling of java/ and kotlin/, so the Track can
publish through the existing _track-ci.yml pipeline by adding a caller workflow
with track_dir: "typescript".

exercise/ is the monolith with TODOs open; solution/ is the decoupled version.
Both typecheck clean, and both were run end to end against a dev server:

  monolith   TXN-A COMPLETED/LOW, TXN-B COMPLETED/MEDIUM,
             TXN-C DECLINED_COMPLIANCE/HIGH
  decoupled  async Nexus check starts complianceWorkflow in compliance-namespace,
             TXN-B parks for review, sync submitReview lands the Update, and
             payment-TXN-B completes with CONF-TXN-B

The Deck for this workshop lives in temporalio/temporal-devdays-ts per that
repo's ADR-0001. The Instruqt Track is not ported yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 28, 2026 18:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Ports the “decouple-monolith” workshop content to TypeScript by adding a full typescript/ sibling implementation (exercise + solution) that demonstrates decoupling Payments and Compliance via Temporal Nexus, matching the existing Java/Kotlin behavior and lab flow.

Changes:

  • Adds TypeScript exercise scaffolding with TODO-driven Nexus contract, handler, worker wiring, and workflow call sites.
  • Adds a complete TypeScript solution implementing Nexus service contract + remote compliance calls, including a review submission path.
  • Introduces TypeScript project configuration and Node ignores (node_modules/, lib/) for the repo.

Reviewed changes

Copilot reviewed 30 out of 33 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
.gitignore Ignores Node/TypeScript build artifacts (node_modules/, lib/).
typescript/decouple-monolith/exercise/tsconfig.json TypeScript compiler configuration for the exercise project.
typescript/decouple-monolith/exercise/package.json Exercise npm scripts and dependencies for Temporal + Nexus.
typescript/decouple-monolith/exercise/src/shared/types.ts Shared cross-team data types and shared constants (queues/namespaces/endpoint).
typescript/decouple-monolith/exercise/src/shared/nexus-service.ts Exercise Nexus service contract scaffold with TODOs.
typescript/decouple-monolith/exercise/src/payments/workflows.ts Monolith workflow with TODO to replace compliance Activity with Nexus call; review caller TODO.
typescript/decouple-monolith/exercise/src/payments/worker.ts Monolith Payments worker (also hosts Compliance activities) with TODO to decouple.
typescript/decouple-monolith/exercise/src/payments/starter.ts Starts multiple payment workflows concurrently and prints results.
typescript/decouple-monolith/exercise/src/payments/review-starter.ts Starts a workflow that submits a review decision (exercise path).
typescript/decouple-monolith/exercise/src/payments/gateway.ts Simulated payment gateway with validation + execution behavior.
typescript/decouple-monolith/exercise/src/payments/activities.ts Payments activities wrapping the gateway (async for proxyActivities).
typescript/decouple-monolith/exercise/src/compliance/workflows.ts Compliance workflow that waits for human review via update for MEDIUM risk.
typescript/decouple-monolith/exercise/src/compliance/worker.ts Compliance worker scaffold with TODO to register Nexus services.
typescript/decouple-monolith/exercise/src/compliance/nexus-handler.ts Exercise Nexus handler scaffold with TODOs for operations.
typescript/decouple-monolith/exercise/src/compliance/checker.ts Deterministic compliance checker rules used by Compliance.
typescript/decouple-monolith/exercise/src/compliance/activities.ts Compliance activity wrapper calling the checker.
typescript/decouple-monolith/solution/tsconfig.json TypeScript compiler configuration for the solution project.
typescript/decouple-monolith/solution/package.json Solution npm scripts and dependencies for Temporal + Nexus.
typescript/decouple-monolith/solution/package-lock.json Locked dependency graph for reproducible installs.
typescript/decouple-monolith/solution/src/shared/types.ts Shared cross-team data types and shared constants (queues/namespaces/endpoint).
typescript/decouple-monolith/solution/src/shared/nexus-service.ts Implemented Nexus service contract (checkCompliance + submitReview).
typescript/decouple-monolith/solution/src/payments/workflows.ts Payments workflow uses Nexus client for compliance; includes review caller workflow.
typescript/decouple-monolith/solution/src/payments/worker.ts Decoupled Payments worker running Payments code only.
typescript/decouple-monolith/solution/src/payments/starter.ts Starts multiple payment workflows concurrently and prints results.
typescript/decouple-monolith/solution/src/payments/review-starter.ts Submits a review decision via a caller workflow (solution path).
typescript/decouple-monolith/solution/src/payments/gateway.ts Simulated payment gateway with validation + execution behavior.
typescript/decouple-monolith/solution/src/payments/activities.ts Payments activities wrapping the gateway (async for proxyActivities).
typescript/decouple-monolith/solution/src/compliance/workflows.ts Compliance workflow that waits for human review via update for MEDIUM risk.
typescript/decouple-monolith/solution/src/compliance/worker.ts Decoupled Compliance worker registering the Nexus service handler.
typescript/decouple-monolith/solution/src/compliance/nexus-handler.ts Implements Nexus operations: async check starts workflow; sync submitReview sends update.
typescript/decouple-monolith/solution/src/compliance/checker.ts Deterministic compliance checker rules used by Compliance.
typescript/decouple-monolith/solution/src/compliance/activities.ts Compliance activity wrapper calling the checker.
Files not reviewed (2)
  • typescript/decouple-monolith/exercise/package-lock.json: Generated file
  • typescript/decouple-monolith/solution/package-lock.json: Generated file

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +2 to +4
"extends": "@tsconfig/node22/tsconfig.json",
"version": "5.6.3",
"compilerOptions": {
Comment on lines +2 to +4
"extends": "@tsconfig/node22/tsconfig.json",
"version": "5.6.3",
"compilerOptions": {
Adds typescript/instruqt, typescript/sandbox, typescript/diagrams, and the CI
caller, completing the TypeScript sibling of kotlin/.

Track identity is deliberately absent: track.yml has no id and no checksum, no
assignment.md carries an id, and no tab carries an id. The slug is new
(nexus-typescript-decouple-monolith). Copying the Kotlin ids would have made the
first push overwrite the Kotlin track, which CI force-pushes on merge to main.

Verified: instruqt track validate passes; all 24 tab-N button references resolve
to the correct tab by label; every /root/workshop path referenced by a lifecycle
script exists; all lifecycle scripts pass bash -n; the diagram server serves the
diagram, 404s correctly, and rejects path traversal.

Two lessons change shape in TypeScript and the assignments say so:

- Challenge 2 now ends with a FAILING typecheck by design. Declaring an Operation
  obliges a matching handler at compile time (TS2345), where Kotlin failed at
  Worker startup instead.
- Challenge 3's proof of registration inverts. There is no "Nexus Poller" line to
  look for; instead the absence of "No Nexus services registered, not polling for
  Nexus tasks" is the signal.

Also renumbers exercise TODOs 6 and 7 so they read in challenge order, with the
coupling deletion last.

The sandbox image is not built yet. Build and digest-pin it per the comment in
.github/workflows/build-and-push-typescript.yml before pushing the track.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@nadvolod nadvolod changed the title Port the Nexus decouple-monolith code to TypeScript Port the Nexus workshop to TypeScript: code + Instruqt track Aug 28, 2026
nadvolod and others added 20 commits August 28, 2026 14:50
Adds typescript/.dockerignore and pins config.yml to the image that was built
and pushed to ghcr.io/nadvolod/edu-nexus-typescript-sandbox.

Digest: sha256:ce2f1b9f1671a9f6b61f7042cc417e95ca887d09acbb62ec209ae5b7e540ea44

The .dockerignore is load-bearing, not tidiness. The build context is typescript/
and the Dockerfile COPYs decouple-monolith/, which carries 292 MB of node_modules
per tree. Without the ignore the build ships ~600 MB of context and bakes in
host-built artifacts that are the wrong architecture: the host is arm64, the
image amd64, and @temporalio ships a native core bridge. npm ci reinstalls them
inside the image anyway. Context drops from 584 MB to 0.3 MB.

Verified against the pushed image, not just the build:
  uname -m           x86_64
  node               v22.23.2, npm 10.9.8
  temporal           1.7.2 (Server 1.31.1)
  code-server        present, both user-data-dirs seeded
  workshop trees     13 .ts files and 98 node_modules entries each
  diagrams           monolith-architecture.html + serve-diagrams.mjs present
  monolith run       worker starts, TXN-A COMPLETED/LOW, TXN-B COMPLETED/MEDIUM,
                     TXN-C DECLINED_COMPLIANCE/HIGH

That last one matters most: it proves the native core bridge resolved for
linux/amd64 from a lockfile generated on darwin-arm64.

instruqt track validate still passes with the digest pinned.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds `maintenance: true` to track.yml. A first publish should not be reachable
by a learner before a human has walked it, and the repo's push guardrail only
permits an agent to publish a track that is in maintenance mode — pushing to a
live track is human-owned. Flip this to false to take the track live.

The publish itself did not complete: Instruqt reported "Could not find the
image". Cause confirmed by anonymous manifest fetch against GHCR:

  nadvolod/edu-nexus-kotlin-sandbox      HTTP 200  (public)
  nadvolod/edu-nexus-typescript-sandbox  HTTP 403  (private)

A newly pushed GHCR package is private by default, and Instruqt pulls
anonymously. The package needs to be made public, matching the Kotlin one.
GitHub exposes no REST endpoint for package visibility, so that is a web-UI
action.

No partial state was left behind: no id or checksum was written back to
track.yml or any assignment.md, so the track was not created. The Kotlin track
is untouched (id yopxoc2jf3jb, checksum 13209538472498183870).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Published to Instruqt as temporal/nexus-typescript-decouple-monolith, in
maintenance mode. These ids are assigned by the server and written back into the
files; committing them is what makes a later publish update this track rather
than create a second one.

  track      fxx1wuc1irwu   checksum 7568052846915990700
  challenges nwdcc4mlob8f, ynazxaoijxil, lwb2jpje3u96, xbklygolip1u, hcijwjx63wn2
  plus 7 tab ids per challenge

Two failed attempts preceded this, both worth recording:

1. Instruqt reported "Could not find the image". The GHCR package was private by
   default; the Kotlin one is public. Confirmed by anonymous manifest fetch
   (403 vs 200) and fixed by making the package public.

2. The retry then reported "There are remote changes for this track". Attempt 1
   had created a bare stub server-side before aborting on the image: id assigned,
   title "title", empty description, no tags, and a config.yml with no containers
   at all. Pulling into a throwaway copy showed only track.yml and config.yml
   differed, so --force overwrote a stub of our own making, not real content.

Verified afterwards:
  - every tab-N button reference still resolves to the right tab by label, which
    is the check validate-track.sh silently stops performing once ids exist
  - the Kotlin track is untouched: pulling kotlin/instruqt reports "Track is up
    to date" with no .remote files, and kotlin/ has no working-tree changes

The track is in maintenance mode. Flip `maintenance` to false to take it live.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The README described the Kotlin track in depth and said nothing about TypeScript.
Adds a "Language tracks" table pointing at both, a note that the slide decks live
in temporalio/temporal-devdays-ts rather than here, and a TypeScript section that
mirrors the Kotlin one.

The section leads with the three lessons that do NOT survive the port, because
those are the parts a person who has delivered the Java or Kotlin version will
otherwise get wrong:

  - challenge 2 deliberately ends on a failing typecheck, since
    ServiceHandlerFor<Ops> obliges a handler at compile time
  - challenge 3's proof of registration inverts: the absence of "No Nexus services
    registered" is the signal, there is no Nexus Poller line
  - the Endpoint is named at the call site, not on the Worker

It also records what cost time during this port and is invisible from the files:
the GHCR package must be public or Instruqt reports only "Could not find the
image"; proxyActivities silently types away synchronous Activities and surfaces
as "Type 'Symbol' has no call signatures" at the call site; a setHandler
validator must take the same arguments as its handler; visibility queries use the
camelCase function name; and the Kotlin and TypeScript workshops cannot share one
dev server because both poll compliance-risk.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An attendee of this workshop has not taken any other version of it, so telling
them "this differs from the Java or Kotlin version" is noise at best and
confusing at worst. Removes every such reference from attendee-facing material
and from the infrastructure comments alongside it.

Removed outright:
  - challenge 02: the paragraph contrasting the compile-time failure with a
    runtime "Missing handlers for service operations"
  - challenge 04: the whole section "A Note if You Have Taken the Java or Kotlin
    Version"

Rewritten to stand on their own, keeping the teaching and dropping the comparison:
  - challenge 02 now says the contract is enforced at compile time, before
    anything runs
  - challenge 04 gains "Where the Endpoint Name Lives", which makes the boundary
    argument positively: the Workflow names a contract and an Endpoint and
    nothing else, and the Registry resolves the rest
  - TODO 5 in payments/workflows.ts, which a learner reads in the editor
  - shared/types.ts and payments/activities.ts in both trees
  - config.yml memory note, Dockerfile extension note, the diagram-server note in
    track_scripts/setup-workshop, and the comments in two lifecycle scripts

The slide deck is updated in the same way in temporalio/temporal-devdays-ts.

The README keeps its comparison deliberately: it documents a repo that contains
java/, kotlin/ and typescript/ side by side, it is maintainer documentation no
attendee reads, and the porting divergences are the reason that section exists.
One sentence there was made false by this change — it claimed the assignments
carry a note for people who have delivered another version — and is corrected,
now stating the standalone rule so it survives the next edit.

Both trees typecheck. Source changed, so the sandbox image was rebuilt and
re-pinned:

  sha256:e86dbaffb0ad1dbfc17730cb2eafb4ac138b09d12946c94e9cce3ea2ee41c9e6

Verified in the pushed image itself: zero java/kotlin/jvm matches in
payments/workflows.ts and shared/types.ts under /opt/workshop. Track republished
in maintenance mode; all tab-N button references still resolve by label
afterwards.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Removes the `maintenance:` key entirely rather than setting it to false. The
serializer omits false values, so `maintenance: false` would silently vanish on
the next publish and read later as a forgotten setting. An absent key IS the off
state, which is why kotlin/instruqt/track.yml has no such key either. Both tracks
now have the same shape.

Off is what a live workshop wants: the track is launchable by anyone with the
link rather than by owners only.

This is the committed intent. Applying it to the live track is a separate step
that this repo's guardrail reserves for a human, and it cannot be done from an
agent session: the hook refuses any command whose text contains the word it
guards while a track is out of maintenance mode, which covers both the Instruqt
publish and `git push`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
checkCompliance is the harder of the two Nexus Operations and the one whose shape
carries the lesson. Asking a learner to invent WorkflowRunOperationHandler from a
blank function, then invent the opposite shape immediately after, was two hard
problems where one worked example plus one contrast does the teaching better.

So checkCompliance is now written in the exercise tree, annotated to point at the
parts submitReview will mirror: it wraps a function of (ctx, input), it calls
startWorkflow, and the Workflow ID is business-meaningful so a retry re-attaches
instead of starting a second check. The learner writes submitReview, which is the
opposite shape on the same Service — fast work answers in the call, slow work
starts a Workflow and returns a reference.

TODOs renumbered 1 to 6 so there is no gap where the old TODO 2 was:

  1  contract                      (unchanged, challenge 2)
  -  checkCompliance handler       now a worked example
  2  submitReview handler          was 3
  3  nexusServices on the Worker   was 4
  4  Nexus client in the Workflow  was 5
  5  reviewCallerWorkflow          was 6
  6  delete the coupling           was 7

Challenge 2's quoted compiler error is re-captured, not adjusted by hand. With one
handler already present it now names exactly the missing one:

  Property 'submitReview' is missing in type
  '{ checkCompliance: WorkflowRunOperationHandler<...>; }' but required in type
  'ServiceHandlerFor<...>'.

which is a sharper version of the same lesson than the previous two-name error.

Challenge 3's assignment is restructured to match: "Read the Worked Example First",
then "Write the Other Handler". The shape table marks which Operation is given and
which is the learner's. README's challenge table records the same.

Both trees typecheck, instruqt track validate passes, all tab-N button references
still resolve by label, and the track still contains no cross-language references.

NOT YET APPLIED TO THE LIVE TRACK. Source under decouple-monolith/ changed, so the
sandbox image must be rebuilt and re-pinned before this ships — the sandbox boots a
pinned digest, so republishing the track alone would serve the old exercise code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The exercise tree changed when checkCompliance became a worked example, and the
sandbox boots a pinned digest, so the previous image would have served attendees
the old blank handler no matter what the track said.

  sha256:8c0d7059211849d86604b729a66eb4075e150f10d8389a738949b375964060fe

Verified inside the pushed image rather than from the build log:

  worked example present in nexus-handler.ts      yes
  checkCompliance implemented                     yes
  submitReview left as TODO 2                     yes
  submitReview NOT implemented                    confirmed
  TODO numbers across the tree                    1 2 3 4 5 6, contiguous
  npx tsc --noEmit inside the sandbox             passes

instruqt track validate passes with the new digest pinned.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    sha256:324b5db3f92d39d6b896dd5ff74fbcf431ba9ef7cff79225c3de89e4a9f5c732

files.autoSave was never set, while every assignment tells the learner "The
editor saves as you type. There is no save button." That was simply false: VS
Code defaults autoSave to off, so a learner edits a file, clicks the Terminal
tab, runs the Worker and gets the OLD code back. It presents as "my fix did not
work" and there is nothing on screen to explain it. Now afterDelay at 300ms,
which makes the sentence true.

The same gap exists in kotlin/instruqt with the same claim in its assignments.
Not touched here.

IntelliSense and code help:

  typescript.tsdk                     pinned to the workspace TypeScript (5.9.3)
                                      so editor diagnostics match npx tsc. This
                                      matters because challenge 2 quotes an exact
                                      TS2345 message and a version drift would
                                      make the quote wrong.
  disableAutomaticTypeAcquisition     ATA fetches @types from npm and can stall
                                      tsserver in a sandbox. Every type needed is
                                      already in node_modules.
  inlay hints                         parameter names on literals, return types,
                                      property types. The Temporal API is
                                      unfamiliar; seeing args:/workflowId: inline
                                      is most of the value.
  importModuleSpecifier: relative     matches how this codebase imports.

Error Lens is installed from Open VSX. That is pedagogical rather than cosmetic:
challenge 2 deliberately ends on a compile error and the lesson is that the
contract is enforced before anything runs. An error the learner has to hover to
read is a lesson half-delivered. The install is guarded with a warning fallback
so a flaky registry cannot fail the image build.

Ergonomics: sticky scroll on, minimap off (more code visible when projected),
enablePreview off so single-click stops replacing the open tab, compactFolders
off so src/payments does not collapse into one line.

Deliberately NOT added: formatOnSave or Prettier. Reformatting half-written code
mid-exercise is disorienting and would fight the TODO comment layout.

The editor is still rooted at src/ rather than the project root. Verified rather
than assumed: tsserver walks up past the workspace root and does find
exercise/tsconfig.json, so strict, ES2022 and Node16 resolution are all in effect.

Verified in the pushed image: autoSave present, tsserver.js present at the pinned
tsdk path so the language service cannot fail to start, and errorlens listed by
code-server --list-extensions. instruqt track validate passes with the new digest.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Checksum writeback from the publish that took the track out of maintenance
mode. Committed deliberately: the checksum is how the CLI detects drift, and
leaving a stale one committed makes the next publish report "There are remote
changes for this track" and demand --force for no real reason.

The README warns against committing checksum-only writeback FROM CI, where it
would produce a commit on every merge. This is a one-off after a manual publish.

Verified: pulling the track into a throwaway copy reports "Track is up to date"
with no .remote files, so local and remote match exactly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    sha256:079c495e5ba873fa16e8f27a9736d75ee0e0d632e2e3bcda6cfbea8030f445ed

Reported from a live sandbox: writing submitReview exactly as the TODO 2 skeleton
shows it fails with

    error TS7006: Parameter '_ctx' implicitly has an 'any' type.

_ctx is a red herring. Reproduced and confirmed in both directions:

  contract empty      ServiceHandlerFor<{}> gives no contextual type, so the
                      parameter falls back to any and strict rejects it
  contract declared   the identical code compiles, exit 0

So the cause is TODO 1 being unfinished, and the error names a file that is not
where the fix goes. A learner can reach that state legitimately, because
challenge 2's check is exit 0 and clicking Check without doing TODO 1 passes.

Fixed by explaining it where it is actually seen, in both places:

  - above TODO 2 in nexus-handler.ts, the file open in the editor
  - under the typecheck step in challenge 3's assignment, quoting the exact error

Written as a lesson rather than a warning, because it demonstrates the point the
workshop is making: handler parameter types are inferred FROM the contract, so
the contract is not documentation sitting beside the code. Delete it and the
handlers stop type checking.

Deliberately NOT changed: challenge 2's check is still exit 0. Giving it a
diagnostic would catch this at the right moment and would match the exception
already made for challenge 4, whose failure mode is also silent. It is left alone
because instant-pass checks are a documented, deliberate policy in the README, and
gating challenge 2 nudges a stuck learner toward Skip, which copies solution files
over their work.

Verified in the pushed image: the guidance is present in the file the learner
opens and names shared/nexus-service.ts as the fix; autosave and Error Lens from
the previous build survive; the exercise typechecks inside the sandbox.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reported from a live sandbox: a Go Worker appears in compliance-namespace on task
queue temporal-sys-per-ns-tq, and nothing in the workshop explains it.

It is not ours. Verified in the sandbox image with NO workshop Worker started at
all: temporal-sys-per-ns-tq already has activity and workflow pollers with
identity temporal-system@<host>@compliance-namespace, and the only non-shell
process in the container is `temporal server start-dev`. Temporal Server is
written in Go and runs an internal Worker in every Namespace for its own
background work, so it is present from the moment the dev server starts and sits
at Tasks Processed 0.

Two assignment claims were wrong because of it:

  challenge 3  "Your Worker is listed as Running" — there are TWO Workers listed
               and no way given to tell them apart. Now a table: compliance-risk
               + TypeScript is theirs, temporal-sys-per-ns-tq + Go is Temporal's,
               with the rule that anything on temporal-sys- is the system talking
               to itself.

  challenge 3  "In challenge 1 this Namespace was empty" — it was not, in the
               Workers view. Now says there was no Worker of THEIRS here.

  challenge 1  "Empty. ... nothing runs there" — true of Workflows, false of
               Workers. Now says "No Workflows", and points out the Go Worker
               before the attendee finds it and assumes something is wrong.

Assignment text only, so no image rebuild is needed. instruqt track validate
passes and all tab-N button references still resolve by label.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Checksum writeback from the publish that shipped the TS7006 guidance and the
Go system Worker explanation. Committed so the next publish does not report
"There are remote changes for this track" against a stale value.

Verified: pulling into a throwaway copy reports the track up to date with no
.remote files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    sha256:cb82e47fb3f5874d53685f622282aac85d0214d49b8dc9a52000cfb5042cb8fd

One editor tab, rooted at /root/workshop, so exercise/ and solution/ are in the
same file tree. Six tabs now, not seven.

This also removes a known flake rather than documenting it. Instruqt loads every
service tab's iframe at challenge start including hidden ones, and code-server
cannot lay itself out in a 0x0 iframe. That is why INSTRUQT.md says the Kotlin
track's Solution tab "may still need one refresh". With one editor instance and
one visible iframe there is no second iframe to race.

Four things had to move with it, three of which would have failed silently:

  tab-N is a POSITION. Dropping Solution at index 5 moved Monolith Architecture
  from tab-6 to tab-5. No button pointed at Solution, so exactly one reference
  needed remapping; all 24 were re-verified by label afterwards.

  Source paths were ambiguous the moment solution/ became visible: "open
  payments/workflows.ts" now matches two files, and a learner editing the
  solution copy would watch their changes do nothing. All 11 references in the
  assignments are now full paths, exercise/src/payments/workflows.ts.

  track_scripts/setup-workshop probes the editor for a 200 on
  /?folder=$EX_FOLDER to decide it is ready. EX_FOLDER still pointed at
  exercise/src, which is no longer a URL any tab opens. Now /root/workshop.

  Challenge 3's note told learners "The Solution tab is one click away", a tab
  that no longer exists. Now points at solution/ in the file tree.

The new root exposes package.json, tsconfig.json, lockfiles and the node_modules
symlinks, so files.exclude hides node_modules and lockfiles. That is the change
that required the image rebuild.

Trade-off worth recording: source is two levels deeper than before, since the
root is the workshop directory rather than exercise/src. Judged worth it for a
single code tab plus the removed flake, but it is a real cost to a learner
navigating under time pressure.

Verified in the pushed image: files.exclude present, autosave and pinned tsdk and
Error Lens all survive, both project roots present. instruqt track validate
passes and every tab-N button still resolves by label across all five challenges.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    sha256:417c2f518ef4b5ecf76a8ea588b05e699e01f6d975d501458fb5d5de32a092fb

Three complaints, all fair:

  TODO 4's instructions sat at line 23, above the proxy, while the code they
  described went at lines 54, 72 and 85. You read the whole block, scrolled away,
  and lost it.

  TODO 4 and TODO 5 wrote nearly identical code — createNexusServiceClient plus
  executeOperation — at opposite ends of the file, which reads as duplicated
  busywork rather than a contrast.

  Both were far too verbose. Thirty lines of comment for two lines of code.

Now one TODO 4 with parts (a) and (b), stated once, immediately above the call it
changes. The duplication becomes the point rather than an accident: same client
twice, 'checkCompliance' at '10 minutes' and 'submitReview' at '10 seconds'.
Async Operations budget for the whole call including retries; sync Operations must
answer inside the handler deadline. One Service, two Operations, two budgets.

The old proxy keeps a two-line pointer instead of the essay. workflows.ts is 114
lines, down from 127, with more of it being code.

TODOs renumbered 1 to 5: the coupling deletion in payments/worker.ts was 6.

reviewCallerWorkflow could NOT simply be given away, which is what prompted the
merge. It has to call executeOperation('submitReview', ...), and submitReview does
not exist on the contract until TODO 1 is done, so a pre-written version fails to
compile at challenge 1:

    error TS2769: No overload matches this call.

That breaks both the image build's tsc --noEmit and challenge 1's starter, which
type checks workflows.ts when it imports paymentProcessingWorkflow. The two other
ways out each cost a lesson — routing the review outside Nexus would leave the
sync handler from TODO 2 with no caller, and shipping the contract complete would
remove challenge 2 entirely.

Verified in the pushed image: TODO numbers 1 to 5 with no orphan, TODO 4 carries
both parts, and the exercise typechecks inside the sandbox.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Checksum writeback only. Committed so the next publish does not report remote
changes against a stale value.

Verified: pulling into a throwaway copy reports the track up to date with no
.remote files, and the live track serves image 417c2f51.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    sha256:2ebb4d537d0cbbbeef147c28fe3bfa99cfb9b7c5961b03a1015cd01ee5b33876

Two corrections, both reported from a live sandbox.

One: implementation steps had leaked into challenge 4's assignment.

    Then delete the proxy above it and its `complianceActivities` import.
    Remove `...complianceActivities` from the `activities` object, and delete its import.

Both were already in the TODO comments, three lines from the cursor. The
assignment now names the file and the TODOs and says why the change matters; the
TODO says how. Rule and a grep for the shape added to INSTRUQT.md under Assignment
wording, written to sit alongside "Show the line, do not describe it" rather than
contradict it: code the learner READS belongs in the assignment, code the learner
WRITES belongs in the TODO.

Two: one TODO covering three edits in three places was confusing. A note reading
"TODO 4(a) deletes this proxy" sat on the proxy while the instructions for 4(a)
were thirty lines away at the call site, so the two halves of one instruction
pointed at each other.

Now one action per lettered TODO, each sited on the code it changes:

    TODO 4a   on the Activity proxy to delete
    TODO 4b   on the call site to replace
    TODO 4c   on reviewCallerWorkflow to write

Letters rather than new numbers because the three parts are one idea — putting the
Nexus boundary in. New numbers would imply separate concepts and inflate the count.
Also captured in INSTRUQT.md.

Verified in the pushed image: all three letters present, 4a on the proxy, 4b on the
call site, no stale "TODO 4(" spelling, and the exercise typechecks in the sandbox.

Separately worth recording: the GHCR-package-is-private failure that cost time
during the first publish was ALREADY documented in INSTRUQT.md before it was hit.
The file worked; it was not read past its first section.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    sha256:79ea7e304f7374b5c0db5bc574555294f75778a7a0a895511e5581b1822951d0

The workshop claimed in five places that TypeScript Nexus support is at
Pre-release with experimental APIs. That is wrong, and it was wrong when it
shipped.

TypeScript SDK v1.23.0 release notes, PR #2299 "Mark Nexus as GA":

    Nexus is now generally available (GA) for calling Nexus Operations from
    Workflows and handling Workflow-backed Operations with
    WorkflowRunOperationHandler.

v1.23.0 is the version this workshop pins, and those two capabilities are exactly
what the lab builds. The "Pre-release" line came from the samples-typescript
README, which is stale relative to the SDK it documents.

Checked rather than swapped one claim for another. In @temporalio/nexus@1.23.0 the
APIs used here — WorkflowRunOperationHandler, startWorkflow, getClient — carry no
@experimental tag. The tags that remain in that package are on APIs this lab does
not touch: TemporalOperationHandler and the cancel-options interfaces.

Two real caveats survive and are now stated instead of the wrong one:

  nexus-rpc is separately versioned, still 0.0.3, and does tag service(),
  operation() and serviceHandler() @experimental. An attendee hovering
  nexus.service will see that and reasonably conclude Nexus is experimental. It is
  not; the contract package is. Named in the deck and in shared/nexus-service.ts so
  it is answered before it is asked.

  Standalone Nexus Operations, which run an Operation with no caller Workflow via
  client.nexus.createServiceClient(), ARE still Pre-release and need a special CLI
  build. The workshop does not use them.

Challenge 5 now closes by saying the shape is production-ready rather than warning
the surface will move. Corrected in track.yml, challenge 5, the contract file, the
README, and the deck (temporalio/temporal-devdays-ts).

Worth recording: the split evidence was noted when this first went in — the
@experimental half was verified, the Pre-release half came from a changelog and
the feature guide carried no release-stage banner. The weaker reading shipped
anyway instead of checking the SDK release notes for the pinned version.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…enge

Uses Instruqt's own feedback support rather than a custom tab:

  lab_config:
    feedback_tab_enabled: true     Feedback tab in the lab UI
    feedback_recap_enabled: true   prompt at the end of the track

That choice matters. A Feedback entry added to each challenge's tabs: list would
renumber every tab after it and silently break every [button](tab-N) in the track,
for a tab Instruqt already gives you. Verified against
temporal-community/ai-agents-workshop-v4, which sets both flags and has no
Feedback entry in any tabs: list — it is lab chrome. Tab counts here stay at six
and every button reference still resolves by label.

Each challenge now ends with the same two-line ask, after the closing summary:

  Please share your feedback so we can make better content for you. The Feedback
  tab takes a few seconds, and it is the only way we find out which parts of this
  landed.

It names the tab rather than linking it, because chrome has no tab-N to point at.

Recorded in INSTRUQT.md so the next person does not reach for a custom tab.

Assignments and track.yml only, so no image rebuild. instruqt track validate
passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Checksum writeback only. Both feedback flags survived the publish rewrite
because the serializer keeps true values and omits false ones — the same
mechanism that makes maintenance: false vanish. If these are ever turned off,
expect the keys to disappear from the file rather than persist as false.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@nadvolod
nadvolod merged commit 22f57dc into main Aug 31, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants