You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: instruqt/01-hello-tailnet/assignment.md
+15-13Lines changed: 15 additions & 13 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -44,22 +44,24 @@ timelimit: 900
44
44
enhanced_loading: null
45
45
---
46
46
47
+
# Exercise 1: Hello `tailnet`
48
+
47
49
Your first Workflow on the shared Temporal server, accessed through the Tailscale network.
48
50
49
-
# Background
51
+
##Background
50
52
51
53
A Temporal dev server is running on a remote VPS, exposed to this Tailscale network via [temporal-ts-net](https://github.com/temporal-community/temporal-ts-net). **Once you join the tailnet in Step 1**, you'll be able to reach it at `temporal-dev:7233` (gRPC) and via the **Temporal UI** tab. (The UI tab may show a connection error until Step 1 completes; that's expected.)
52
54
53
55
The Workflow you'll run gets your exercise environment's public IP address, then geolocates it. This exercise is the foundation the rest of the workshop builds on: every later exercise assumes Workers and the Temporal Server can reach each other over the `tailnet` rather than the public internet.
54
56
55
-
# Environment
57
+
##Environment
56
58
57
59
All code for this exercise lives in `exercises/01_hello_tailnet/`. Inside that directory:
58
60
59
61
-**`practice/`** is where you do your work. Each file has one or more **TODO** comments pointing at the change you need to make.
60
62
-**`solution/`** contains the finished version of every file. If you get stuck or want to double-check your work, compare against the matching file in `solution/`. Don't run from `solution/`, run from `practice/`.
61
63
62
-
# Step 1: Join the `tailnet`
64
+
##Step 1: Join the `tailnet`
63
65
64
66
Your Exercise Environment has the Tailscale client and an auth key available as `$TS_AUTHKEY`.
65
67
@@ -77,17 +79,17 @@ tailscale status
77
79
78
80
You should see all of the devices that are on the tailnet, including the Temporal Server `temporal-dev` in the list.
79
81
80
-
# Step 2: Verify the Temporal Config File
82
+
##Step 2: Verify the Temporal Config File
81
83
82
84
Temporal CLI and SDKs support configuring a Temporal Client using environment variables and TOML configuration files, rather than setting connection options programmatically in your code. This decouples connection settings from application logic, making it easier to manage different environments such as development, staging, and production without code changes.
83
85
84
86
This has already been set up for you in this environment. To verify, open `temporal.toml` in the [button label="Code Editor" background="#444CE7"](tab-0) tab. It's already in place in the workshop directory and the SDK is pointed at it via `TEMPORAL_CONFIG_FILE`. You should see two profiles: `default` (localhost) and `tailnet` (pointing at `temporal-dev:7233`).
85
87
86
-
# Step 3: Configure Your Application to Connect to the `tailnet` Profile
88
+
##Step 3: Configure Your Application to Connect to the `tailnet` Profile
87
89
88
90
Both `worker.py` and `starter.py` currently load the `default` profile, which points at localhost. You need to point them at the `tailnet` profile so they connect to the shared Temporal server. This is a one-line change in each file.
Now both the worker and starter read the `tailnet` profile from `temporal.toml` and connect to the shared Temporal server.
125
127
126
-
# Step 4: Add your name to the Workflow ID
128
+
##Step 4: Add your name to the Workflow ID
127
129
128
130
Workflow IDs must be unique on a Temporal Server, and prefixing yours with your name lets you find your run among everyone else's in the shared Temporal UI. `USER_ID` is already set from the sign-up form as `WORKSHOP_USER_ID` and wired into `starter.py`.
129
131
@@ -141,7 +143,7 @@ id=f"geo-ip-{uuid.uuid4()}",
141
143
id=f"{USER_ID}-geo-ip-{uuid.uuid4()}",
142
144
```
143
145
144
-
# Step 5: Start the Worker
146
+
##Step 5: Start the Worker
145
147
146
148
Now you are ready to run your Workflow. First, start the Worker. This is the process that will execute your Temporal application.
147
149
@@ -156,7 +158,7 @@ You should see: `INFO:root:Connecting to Temporal at temporal-dev:7233`
156
158
157
159
Once it has connected you will see output similar to: `INFO:root:Starting worker on task queue: your-name-hello-tailnet`
158
160
159
-
# Step 6: Run the Workflow
161
+
##Step 6: Run the Workflow
160
162
161
163
Once the Worker has started you are ready to run your Workflow.
162
164
@@ -176,7 +178,7 @@ Your IP address: 257.257.257.257
176
178
Your location: Alderaan, Core Worlds
177
179
```
178
180
179
-
# Step 7: Check the Temporal UI
181
+
##Step 7: Check the Temporal UI
180
182
181
183
Click the [button label="Temporal UI" background="#444CE7"](tab-3) tab and find your Workflows by searching for your user ID. You should see your Workflow, along with all other attendees in the workshop!
182
184
@@ -186,7 +188,7 @@ Click the [button label="Temporal UI" background="#444CE7"](tab-3) tab and find
186
188
187
189
Your Worker is running in your exercise environment hosted in a GCP region. The Temporal Server is running on a VPS in DigitalOcean's San Francisco region. Tailscale provided secure access for the Worker to communicate with the server without exposing the server to the public internet.
188
190
189
-
# Step 8: Route egress through an exit node
191
+
##Step 8: Route egress through an exit node
190
192
191
193
Tailscale also allows for setting an exit node on your `tailnet`, acting as a full tunnel VPN.
192
194
@@ -212,7 +214,7 @@ Unset your exit node before moving on:
Copy file name to clipboardExpand all lines: instruqt/02-explore-tailscale/assignment.md
+15-13Lines changed: 15 additions & 13 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -39,13 +39,15 @@ timelimit: 1200
39
39
enhanced_loading: null
40
40
---
41
41
42
+
# Exercise 2: Exploring Your Tailscale Network
43
+
42
44
Now that you've run a Workflow through the `tailnet`, let's look at the network itself. You'll discover what's on the `tailnet`, see how Tailscale identity works, and run a Go Worker that joins the `tailnet` as its own node via [`tsnet`](https://pkg.go.dev/tailscale.com/tsnet) instead of riding on the Exercise Environment's system client.
43
45
44
-
# Background
46
+
##Background
45
47
46
48
In Exercise 1 your Python Worker reached Temporal through the Exercise Environment's Tailscale client. The environment was on the `tailnet`, and the Worker inherited that connectivity. `tsnet` is the library inside Tailscale that lets a process join the `tailnet` directly, as its own node, with no system-wide install. Every Tailscale binary uses it under the hood, and you can embed it in any Go program. That's what `temporal-ts-net` does to put the Temporal dev server on the `tailnet`, and it's the pattern you'll use in Exercise 4 for the metrics watcher.
47
49
48
-
# Environment
50
+
##Environment
49
51
50
52
Steps 1 through 3 poke at the `tailnet` from the **Worker** terminal and don't touch code. Step 4 takes the system Tailscale client offline so the later `tsnet` work has to stand on its own. Starting in Step 5, the Go code for this exercise lives under `go-hello-tsnet/` in the **Code Editor** tab. Inside that directory:
51
53
@@ -66,7 +68,7 @@ Go and the `tsnet` library are already installed, and the module cache has been
66
68
> tailscale up --auth-key="$TS_AUTHKEY" --hostname="${WORKSHOP_USER_ID}-env"
67
69
> ```
68
70
69
-
# Step 1: See what's on the `tailnet`
71
+
## Step 1: See what's on the `tailnet`
70
72
71
73
Before you add another Worker, it's worth looking at who else is already on the `tailnet`. `tailscale status` lists every node your machine can see, along with its IP, hostname, and connection path.
72
74
@@ -82,7 +84,7 @@ You should see:
82
84
-**`temporal-dev`**, the VPS running the shared Temporal dev server
83
85
-**Other attendee machines**, everyone else in the workshop, each with their own hostname
84
86
85
-
# Step 2: Ping the Temporal Server
87
+
##Step 2: Ping the Temporal Server
86
88
87
89
`tailscale status` tells you what's on the `tailnet`. `tailscale ping` tells you *how* you reach any given node, whether the first packets go through a Tailscale relay (DERP) or straight over a direct encrypted WireGuard path.
88
90
@@ -100,7 +102,7 @@ pong from temporal-dev (100.109.42.22) via 167.71.156.227:35753 in 40ms
100
102
101
103
The first line may say `pong from temporal-dev (100.109.42.22) via DERP(...)`, meaning it was relayed through Tailscale's infrastructure. Subsequent lines (like the sample above) report `via <public-IP>:<port>`, a direct encrypted WireGuard path with no relay. Once the direct path is established, every packet to `temporal-dev` flows over it.
102
104
103
-
# Step 3: Check your Tailscale identity
105
+
##Step 3: Check your Tailscale identity
104
106
105
107
Every node on the `tailnet` has an identity: a machine name, tags, and the user that owns the node. Services on the `tailnet` use this identity to authorize or rate-limit you without any API keys on your side. In Exercise 3 you'll see Aperture use exactly this mechanism to attach your name to every LLM call.
106
108
@@ -112,7 +114,7 @@ tailscale whois $(tailscale ip -4)
112
114
113
115
You should see the machine name, tags, and the Tailscale user associated with this node.
114
116
115
-
# Step 4: Take the system Tailscale client offline
117
+
##Step 4: Take the system Tailscale client offline
116
118
117
119
Steps 1 through 3 leaned on the system `tailscale` binary to explore the `tailnet`. The Go Worker you're about to build joins the `tailnet` on its own via `tsnet`, so it doesn't need the binary at all. Prove that by taking it down now, before the Worker ever runs.
118
120
@@ -130,7 +132,7 @@ tailscale status
130
132
131
133
You should see `Stopped` (or `Logged out`) instead of the `tailnet` nodes from Step 1. Your Exercise Environment is no longer on the `tailnet`. The Go Worker is about to put itself there on its own.
132
134
133
-
# Step 5: Configure the `tsnet.Server`
135
+
##Step 5: Configure the `tsnet.Server`
134
136
135
137
The `tsnet.Server` struct is how a Go program declares that it wants to be a `tailnet` node. You give it a hostname, a state directory, and an auth key, and once you call `Start()` your process has its own `tailnet` IP.
136
138
@@ -152,7 +154,7 @@ tsNode := &tsnet.Server{
152
154
153
155
The rest of the function (`tsNode.Start()`, `tsNode.Up(upCtx)`, the log line) is already in place.
154
156
155
-
# Step 6: Dial Temporal through `tsnet`
157
+
##Step 6: Dial Temporal through `tsnet`
156
158
157
159
The `tsnet.Server` you just configured gives your Worker its own node on the `tailnet`. Now you need to tell the Temporal Go SDK to use it, so the gRPC connection to `temporal-dev:7233` flows through `tsnet` instead of the system network stack.
Every byte the SDK sends now flows through `tsNode.Dial`, which routes over the `tailnet`.
168
170
169
-
# Step 7: Start the Go Worker
171
+
##Step 7: Start the Go Worker
170
172
171
173
With both TODOs filled in, the Worker is ready to join the `tailnet` and connect to Temporal on its own.
172
174
@@ -185,7 +187,7 @@ connected to temporal at temporal-dev:7233 via tsnet
185
187
Starting Go worker on task queue: YOUR-USER-ID-hello-tsnet
186
188
```
187
189
188
-
# Step 8: Confirm the Go Worker is on the `tailnet`
190
+
##Step 8: Confirm the Go Worker is on the `tailnet`
189
191
190
192
The Go Worker should now appear as its own node on the `tailnet`, even with the system Tailscale client still offline from Step 4. To verify from the command line, bring the system client back up and check `tailscale status`.
191
193
@@ -198,7 +200,7 @@ tailscale status | grep -- '-ex2-go-worker'
198
200
199
201
You should see a new row, `<your-user-id>-ex2-go-worker-<suffix>`, separate from the Exercise Environment itself. The Worker joined the `tailnet` on its own via `tsnet` while the system client was offline. That's the whole point of Step 4.
200
202
201
-
# Step 9: Run the Workflow
203
+
##Step 9: Run the Workflow
202
204
203
205
Now trigger the same geo-IP Workflow from Exercise 1. This time the activities execute on the Go `tsnet` Worker you just started.
204
206
@@ -211,7 +213,7 @@ go run . starter
211
213
212
214
You should see your public IP address and location printed, same as Exercise 1, but this time the Worker that executed the activities was the Go `tsnet` Worker, not the Python Worker from Exercise 1.
213
215
214
-
# Step 10: Check the Temporal UI
216
+
##Step 10: Check the Temporal UI
215
217
216
218
Click the [button label="Temporal UI" background="#444CE7"](tab-3) tab and find your `<your-user-id>-hello-tsnet` Workflow. Click into it and look at the worker info on each activity. The task queue is `<your-user-id>-hello-tsnet`, and the worker identity reflects the Go process rather than the Python one from Exercise 1.
217
219
@@ -221,7 +223,7 @@ Click the [button label="Temporal UI" background="#444CE7"](tab-3) tab and find
221
223
222
224
Same Temporal Server, same Workflow, different Worker transport. The Python Worker in Exercise 1 relied on the Exercise Environment's Tailscale client to reach Temporal. The Go Worker you just ran carries its own `tsnet` node inside the process itself, joins the `tailnet` on startup, and dials Temporal through that embedded node.
Copy file name to clipboardExpand all lines: instruqt/03-weather-agent/assignment.md
+13-11Lines changed: 13 additions & 11 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -44,9 +44,11 @@ timelimit: 1500
44
44
enhanced_loading: null
45
45
---
46
46
47
+
# Exercise 3: Python Weather Agent
48
+
47
49
Build a durable AI agent that chains tool calls, with all LLM requests secured through Aperture on the `tailnet`.
48
50
49
-
# Background
51
+
##Background
50
52
51
53
This exercise has two phases.
52
54
@@ -61,7 +63,7 @@ This exercise has two phases.
61
63
62
64
Every LLM call goes through **Aperture** instead of directly to OpenAI. Aperture holds the shared API key, identifies you by your Tailscale identity, and enforces rate limits. The same pattern applies in Exercise 4 with Anthropic's Claude.
63
65
64
-
# Environment
66
+
##Environment
65
67
66
68
All code for this exercise lives in `exercises/03_weather_agent/`. Inside that directory:
67
69
@@ -80,7 +82,7 @@ All code for this exercise lives in `exercises/03_weather_agent/`. Inside that d
80
82
> tailscale up --auth-key="$TS_AUTHKEY" --hostname="${WORKSHOP_USER_ID}-env"
81
83
> ```
82
84
83
-
# Step 1: Route LLM calls through Aperture
85
+
## Step 1: Route LLM calls through Aperture
84
86
85
87
This step begins **Phase A: Tool-Calling**. The change is small but load-bearing. Instead of pointing the OpenAI client at `api.openai.com`, you point it at Aperture, the `tailnet`-only gateway that attaches your Tailscale identity, enforces rate limits, and swaps in the real API key server-side.
86
88
@@ -98,7 +100,7 @@ client = AsyncOpenAI(
98
100
99
101
`APERTURE_URL` is already exported in your environment. The OpenAI client will now send requests to Aperture, which forwards them to OpenAI with the shared API key after attaching your Tailscale identity.
100
102
101
-
# Step 2: Start the Phase A Worker
103
+
##Step 2: Start the Phase A Worker
102
104
103
105
Now start the Worker. This is the process that executes the Workflow and its tool activities.
104
106
@@ -111,7 +113,7 @@ uv run worker.py
111
113
112
114
You should see the Worker connect to Temporal and start listening on its task queue.
113
115
114
-
# Step 3: Run the Phase A Workflow
116
+
##Step 3: Run the Phase A Workflow
115
117
116
118
With the Worker running, trigger the Workflow from the [button label="Starter" background="#444CE7"](tab-2) terminal.
117
119
@@ -128,7 +130,7 @@ You should see the LLM call the weather tool and return results. Click the [butt
128
130
129
131
Your Worker executed a Workflow where the LLM chose to call a tool instead of answering directly. The LLM call itself flowed through Aperture, which authenticated you by your Tailscale identity and forwarded the request to OpenAI with the shared API key. Your code never touched an OpenAI API key.
130
132
131
-
# Step 4: Enable the agentic loop
133
+
##Step 4: Enable the agentic loop
132
134
133
135
This step begins **Phase B: Agentic Loop**. The difference from Phase A is that the Workflow now keeps handing the LLM tool results until the LLM decides it has enough information to answer, instead of stopping after one tool call.
134
136
@@ -140,7 +142,7 @@ while True:
140
142
141
143
This turns the single-shot tool call from Phase A into a loop that repeatedly calls the LLM, executes whichever tool the LLM picked, feeds the result back, and stops only when the LLM decides it's done.
142
144
143
-
# Step 5: Execute the chosen activity dynamically
145
+
##Step 5: Execute the chosen activity dynamically
144
146
145
147
Still in `agent_workflow.py`, find **TODO 3** and replace the empty string with `item.name`:
`item.name` is whichever tool the LLM picked on this iteration, such as `get_ip_address`, `get_location_info`, or `get_weather_alerts`. Temporal runs it as a dynamic activity, so the Worker does not hard-code which tool to call; Temporal dispatches by name.
156
158
157
-
# Step 6: Restart the Worker as the agent Worker
159
+
##Step 6: Restart the Worker as the agent Worker
158
160
159
161
The Phase A Worker registered the single-shot tool-calling Workflow. For Phase B you need the agent Workflow registered, which means restarting the Worker with the `--agent` flag.
160
162
@@ -166,7 +168,7 @@ uv run worker.py --agent
166
168
167
169
You should see the Worker reconnect to Temporal and start listening on its task queue, this time with the agent Workflow registered.
168
170
169
-
# Step 7: Run the agentic Workflow
171
+
##Step 7: Run the agentic Workflow
170
172
171
173
Now ask the agent a question that requires multiple tool calls to answer.
172
174
@@ -182,7 +184,7 @@ Watch the Worker logs. The LLM chains through multiple tools before responding:
182
184
183
185
The LLM made autonomous decisions about which tool to call next, and Temporal recorded every call, input, and output in the Workflow history. If the process had crashed halfway through, Temporal could replay the history on a new Worker and the agent would resume from exactly where it left off, even partway through a multi-tool reasoning chain.
184
186
185
-
# Step 8: Explore the Aperture UI
187
+
##Step 8: Explore the Aperture UI
186
188
187
189
Open the [button label="Aperture UI" background="#444CE7"](tab-4) tab to see every LLM call your Workers made.
188
190
@@ -196,7 +198,7 @@ Click the **Adoption** tab for a cost and token-usage breakdown across models an
196
198
197
199
> **Note:** Every Instruqt machine authenticated using the same `tag:infra`, so the Dashboard and Logs show requests from all attendees, not just yours. In a real deployment, Aperture attributes usage per user via their Tailscale identity from your IDP. Agentic workloads should have their own tags too -- both so Aperture tracks them separately from human users and because zero-trust ACLs depend on a well-defined tag taxonomy to enforce least-privilege access. The workshop `tailnet` has fully open ACLs for simplicity; in production you would give each user and each agent only the access they need.
0 commit comments