<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ArshTechPro</title>
    <description>The latest articles on DEV Community by ArshTechPro (@arshtechpro).</description>
    <link>https://dev.to/arshtechpro</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3258664%2F7a2cc61a-0b4d-4cf8-884e-52f33905cac3.png</url>
      <title>DEV Community: ArshTechPro</title>
      <link>https://dev.to/arshtechpro</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/arshtechpro"/>
    <language>en</language>
    <item>
      <title>OpenMausBot: A Local-First, Open-Source Team of AI Agents in a Chat App</title>
      <dc:creator>ArshTechPro</dc:creator>
      <pubDate>Sun, 08 Nov 2026 19:02:52 +0000</pubDate>
      <link>https://dev.to/arshtechpro/openmausbot-a-local-first-open-source-team-of-ai-agents-in-a-chat-app-2haj</link>
      <guid>https://dev.to/arshtechpro/openmausbot-a-local-first-open-source-team-of-ai-agents-in-a-chat-app-2haj</guid>
      <description>&lt;p&gt;If you use agent CLIs like Claude Code or Codex every day, you have probably wished you could run several of them side by side, give each one a role, and manage them like contacts in a messaging app. That is the idea behind OpenMausBot.&lt;/p&gt;

&lt;p&gt;This post is a practical walkthrough for developers: what it is, how it is built, how to run it in a few minutes, and where you can extend it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is OpenMausBot?
&lt;/h2&gt;

&lt;p&gt;OpenMausBot is an open-source, local-first chat app where every chat is a real AI agent. Each bot in the sidebar is backed by an agent CLI running on your machine, and each one can have its own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;personality and instructions&lt;/li&gt;
&lt;li&gt;model&lt;/li&gt;
&lt;li&gt;computer (a cloud Linux desktop, an isolated local VM, or your own machine)&lt;/li&gt;
&lt;li&gt;set of connected apps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The project describes itself as an independent, open-source alternative to products like Grok Bot, Meta Muse, OpenAI dots, and Cue by Manus. It is not affiliated with xAI, Meta, OpenAI, or Manus. It is also known as MausBot and was formerly called OpenGrokBot.&lt;/p&gt;

&lt;p&gt;Quick facts from the repository:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI&lt;/td&gt;
&lt;td&gt;React, Vite, Tailwind CSS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Desktop shell&lt;/td&gt;
&lt;td&gt;Electron (macOS, Windows, Ubuntu 24.04)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;Apache 2.0 (the &lt;code&gt;enterprise/&lt;/code&gt; folder has its own source-available license)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime requirement&lt;/td&gt;
&lt;td&gt;Node 24 or newer, pnpm for source builds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent engines&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;claude&lt;/code&gt;, &lt;code&gt;codex&lt;/code&gt;, and &lt;code&gt;grok&lt;/code&gt; CLIs, plus custom engines&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Why developers might care
&lt;/h2&gt;

&lt;p&gt;There are three design choices that make this project interesting from an engineering point of view.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Bring your own agents.&lt;/strong&gt; OpenMausBot does not proxy your requests through a hosted service. Bots run on the &lt;code&gt;claude&lt;/code&gt;, &lt;code&gt;codex&lt;/code&gt;, and &lt;code&gt;grok&lt;/code&gt; CLIs already installed and logged in on your machine, so your existing subscriptions and logins are reused. You can point an engine at a custom CLI binary in Settings under Engines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Local first.&lt;/strong&gt; One small harness server listens on &lt;code&gt;127.0.0.1&lt;/code&gt; and owns every agent process. Transcripts, keys, and events are stored in &lt;code&gt;~/.openmausbot&lt;/code&gt;, not in someone else's cloud.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Approvals instead of blind autonomy.&lt;/strong&gt; A permission broker turns risky actions such as shell commands and file edits into inline cards in the chat. You choose Allow or Deny, for cloud and local computers alike.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture in two minutes
&lt;/h2&gt;

&lt;p&gt;OpenMausBot is split into two processes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The app&lt;/strong&gt; (React and Tailwind, port 5199 in dev) holds no transports of its own. It sends typed commands over HTTP and folds a single Server-Sent Events (SSE) stream into state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The harness server&lt;/strong&gt; (&lt;code&gt;127.0.0.1:8799&lt;/code&gt;) owns every agent process. It normalizes each provider's native protocol into one canonical runtime event stream, which is logged per thread as NDJSON.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;App (React + Tailwind, :5199)
   |  HTTP commands          ^  one SSE stream
   v                         |
Harness server (127.0.0.1:8799)
   |-- Driver registry --&amp;gt; claude CLI / codex CLI / grok CLI
   |-- Event bus (SSE)
   |-- Permission broker &amp;lt;-- permission requests from agents
   |-- Boat API         --&amp;gt; cloud computer
   |-- Composio session --&amp;gt; Gmail, Slack, GitHub, ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The main layers map to folders in the repo:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Drivers&lt;/td&gt;
&lt;td&gt;&lt;code&gt;server/drivers/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;One per provider, over stream-JSON, JSON-RPC, or ACP. Unknown drivers degrade to "unavailable" instead of crashing the fleet.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Harness&lt;/td&gt;
&lt;td&gt;&lt;code&gt;server/harness/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Registry (configs to live instances) and the fan-in event bus.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API&lt;/td&gt;
&lt;td&gt;&lt;code&gt;server/index.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bots, turns, approvals, model catalog, computer lifecycle, connectors, config over HTTP and SSE.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Voice&lt;/td&gt;
&lt;td&gt;&lt;code&gt;server/tts/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Text-to-speech providers; cloud keys stay on the server.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App&lt;/td&gt;
&lt;td&gt;&lt;code&gt;src/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The chat shell: server-backed store, one reducer, no client-side transports.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Desktop&lt;/td&gt;
&lt;td&gt;&lt;code&gt;electron/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Desktop shells with an embedded harness.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you have built anything with an event-sourced UI, this will feel familiar: commands go in over HTTP, events come out over one stream, and the client is a reducer over those events.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get it running
&lt;/h2&gt;

&lt;p&gt;You have three options.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 1: Download a release
&lt;/h3&gt;

&lt;p&gt;Released builds embed the harness server, so there is no separate server setup. Builds are available for macOS (Apple silicon and Intel), Windows x64, and Ubuntu 24.04 x64 (beta, as a &lt;code&gt;.deb&lt;/code&gt; or AppImage). Grab them from the &lt;a href="https://github.com/milind-soni/OpenMausBot/releases/latest" rel="noopener noreferrer"&gt;latest release&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Note that the Windows installer is not code-signed yet, so SmartScreen will show "unknown publisher". Use More info, then Run anyway.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 2: Run from the terminal with npm
&lt;/h3&gt;

&lt;p&gt;This is the fastest path if you just want to try it, and it works on a server too. You need Node 24 or newer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; openmausbot
openmausbot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also skip the global install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx openmausbot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First launch walks you through arrow-key prompts: choose AI access, sign in or paste a hidden API key, choose a model, and optionally connect a phone. The next run reuses your saved setup. Useful flags and commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openmausbot &lt;span class="nt"&gt;--no-open&lt;/span&gt;     &lt;span class="c"&gt;# do not open the browser automatically&lt;/span&gt;
openmausbot setup         &lt;span class="c"&gt;# reconfigure without resetting bots or conversations&lt;/span&gt;
npx openmausbot serve     &lt;span class="c"&gt;# background service for a VPS or always-on machine&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Option 3: Build from source
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/milind-soni/OpenMausBot &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;OpenMausBot
pnpm &lt;span class="nb"&gt;install

&lt;/span&gt;pnpm dev:server    &lt;span class="c"&gt;# harness server on 127.0.0.1:8799&lt;/span&gt;
pnpm dev           &lt;span class="c"&gt;# app on http://127.0.0.1:5199&lt;/span&gt;
pnpm dev:desktop   &lt;span class="c"&gt;# Electron shell; keep the two commands above running&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Requirements: macOS, Windows, or Ubuntu 24.04 x64, Node 24+, pnpm, and at least one agent CLI (&lt;a href="https://claude.com/claude-code" rel="noopener noreferrer"&gt;&lt;code&gt;claude&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://github.com/openai/codex" rel="noopener noreferrer"&gt;&lt;code&gt;codex&lt;/code&gt;&lt;/a&gt;, or &lt;a href="https://x.ai/cli" rel="noopener noreferrer"&gt;&lt;code&gt;grok&lt;/code&gt;&lt;/a&gt;) installed and logged in. Installed CLIs show up in the model picker automatically.&lt;/p&gt;

&lt;p&gt;Useful scripts while developing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm typecheck        &lt;span class="c"&gt;# app and server&lt;/span&gt;
pnpm &lt;span class="nb"&gt;test&lt;/span&gt;             &lt;span class="c"&gt;# unit, driver, API, and desktop capability tests&lt;/span&gt;
pnpm build            &lt;span class="c"&gt;# typecheck and production build&lt;/span&gt;
pnpm check:electron   &lt;span class="c"&gt;# syntax-check Electron main and preload files&lt;/span&gt;
pnpm package:mac      &lt;span class="c"&gt;# DMG and ZIP (needs Swift/Xcode tools)&lt;/span&gt;
pnpm package:win      &lt;span class="c"&gt;# installer and ZIP&lt;/span&gt;
pnpm package:linux    &lt;span class="c"&gt;# .deb, AppImage, verified CUA runtime&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Optional credentials
&lt;/h2&gt;

&lt;p&gt;Local chat works without any extra keys. Add these in App Settings (the gear icon in the sidebar footer) only when you want the matching feature:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Credential&lt;/th&gt;
&lt;th&gt;What it enables&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Composio project key&lt;/td&gt;
&lt;td&gt;Connect Gmail, GitHub, Slack, Notion, and other apps as tools for your bots&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boat API key&lt;/td&gt;
&lt;td&gt;Give bots an isolated remote Linux computer with a desktop and terminal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ElevenLabs or Fish Audio key&lt;/td&gt;
&lt;td&gt;Read replies aloud and call your bots&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Composio and Boat are third-party services with their own accounts and terms. Boat is paid after its trial, so a cloud computer can incur charges.&lt;/p&gt;

&lt;h2&gt;
  
  
  Features worth knowing about
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Model picker per bot.&lt;/strong&gt; Claude and Codex models appear side by side, and you can switch a bot's model mid-conversation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Computers for bots.&lt;/strong&gt; A bot can use a cloud desktop, an isolated Local VM, or your own computer. Control of your own machine is explicit opt-in and available on macOS and Ubuntu Xorg. On Ubuntu Wayland it is currently disabled behind a safety gate (tracked in issue #345).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Channels.&lt;/strong&gt; Separate Work, Personal, and project contexts without cloning bots. Each channel has its own transcript, shared instructions, working folder, and roster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Teams from one Markdown file.&lt;/strong&gt; A team package uses YAML frontmatter plus a normal Markdown playbook, so the app can install it in one click while humans can still read it. Connections stay off until you approve them, routines arrive paused, and packages never carry credentials, conversations, or memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Triggers.&lt;/strong&gt; Schedules and webhooks let a bot act without you. Schedules can run once, on selected weekdays, or every 5 to 1,440 minutes. Interval schedules skip a run if the previous one is still active, so slow work cannot pile up in a queue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Voice.&lt;/strong&gt; Spoken replies and calls through ElevenLabs, Fish Audio, Grok, built-in Mac voices, or a local Chatterbox server. Calls are macOS-only for now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automate it: webhooks, CLI, and MCP
&lt;/h2&gt;

&lt;p&gt;This is where it gets useful for developers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Webhook triggers
&lt;/h3&gt;

&lt;p&gt;OpenMausBot starts a webhook-only receiver on &lt;code&gt;127.0.0.1:8800&lt;/code&gt; by default. Set &lt;code&gt;OMB_WEBHOOK_PORT&lt;/code&gt; to change it. Key points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The webhook secret is shown once, when the trigger is created or rotated.&lt;/li&gt;
&lt;li&gt;Bearer authentication is recommended so the secret stays out of URLs and access logs.&lt;/li&gt;
&lt;li&gt;The receiver exposes only &lt;code&gt;/health&lt;/code&gt; and secret &lt;code&gt;/hooks/...&lt;/code&gt; endpoints. It never exposes the broader API.&lt;/li&gt;
&lt;li&gt;The app must be running to accept a delivery. For public delivery, proxy only this receiver through a hosted relay or something like Tailscale Funnel.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Headless CLI for scripts and CI
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;openmausbot&lt;/code&gt; command can drive a running workspace without a screen. It returns one JSON document with &lt;code&gt;--json&lt;/code&gt; and uses exit codes a script can branch on.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openmausbot team apply team.yaml
openmausbot run &lt;span class="s2"&gt;"Room Lead"&lt;/span&gt; &lt;span class="s2"&gt;"Rehearse the launch of https://example.com"&lt;/span&gt; &lt;span class="nt"&gt;--export&lt;/span&gt; ./out
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  MCP server
&lt;/h3&gt;

&lt;p&gt;OpenMausBot ships a stdio MCP server so external clients such as Claude Desktop and Cursor can orchestrate your bots. The tool surface is deliberately bounded: inspect bots and channels, read and search transcripts, create and configure bots, channels, and tasks, send work, wait for completion, switch models, and interrupt turns.&lt;/p&gt;

&lt;p&gt;It does not expose approval grants, deletion, arbitrary settings, credentials, or computer lifecycle. That is a sensible boundary, because it means an outside client cannot approve its own risky actions. Setup details are in the &lt;a href="https://github.com/milind-soni/OpenMausBot/blob/main/docs/mcp-server.md" rel="noopener noreferrer"&gt;MCP docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extending it
&lt;/h2&gt;

&lt;p&gt;You have three extension paths, in increasing order of effort.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Add an MCP tool server with zero code.&lt;/strong&gt; See &lt;code&gt;docs/custom-mcp-servers.md&lt;/code&gt; in the repo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a custom engine with config only.&lt;/strong&gt; Any ACP-speaking CLI or OpenAI-compatible endpoint plugs in through configuration. See &lt;code&gt;docs/custom-engines.md&lt;/code&gt;. The docs also cover running bots on OpenRouter, Fireworks AI, DeepSeek, Cline, or your own OpenAI- or Anthropic-compatible provider.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write a new driver.&lt;/strong&gt; The driver interface in &lt;code&gt;server/contracts.ts&lt;/code&gt; is deliberately small. Adding a provider is one file in &lt;code&gt;server/drivers/&lt;/code&gt; plus a one-line registration.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want to contribute, read &lt;code&gt;CONTRIBUTING.md&lt;/code&gt; and &lt;code&gt;LICENSING.md&lt;/code&gt; first. The repo uses a CLA and describes how contributions are signed off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security mindset
&lt;/h2&gt;

&lt;p&gt;Any tool that lets an agent run shell commands and control a computer deserves care. Reasonable defaults when you try it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep bots on Ask mode (approval cards) until you trust a workflow.&lt;/li&gt;
&lt;li&gt;Start with the cloud computer or Local VM before pointing a bot at your own machine.&lt;/li&gt;
&lt;li&gt;Connect only the apps a given bot needs.&lt;/li&gt;
&lt;li&gt;Review a team package before importing it from someone else.&lt;/li&gt;
&lt;li&gt;If you expose it to the internet, use a managed HTTPS endpoint with pairing, Tailscale, or your own reverse proxy, never a bare open port.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Repository: &lt;a href="https://github.com/milind-soni/OpenMausBot" rel="noopener noreferrer"&gt;github.com/milind-soni/OpenMausBot&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Website: &lt;a href="https://www.openmausbot.com" rel="noopener noreferrer"&gt;openmausbot.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Self-hosting guide: &lt;code&gt;docs/self-hosting.md&lt;/code&gt; and &lt;code&gt;docs/deploy-vps.md&lt;/code&gt; in the repo&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you try it, start with &lt;code&gt;npx openmausbot&lt;/code&gt;, create one bot, and watch the approval cards while it works. That is the fastest way to see why the architecture is built the way it is.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>typescript</category>
      <category>agents</category>
    </item>
    <item>
      <title>NVKE: Run Your Own Managed GPU Kubernetes Service on Bare Metal</title>
      <dc:creator>ArshTechPro</dc:creator>
      <pubDate>Sun, 08 Nov 2026 18:56:30 +0000</pubDate>
      <link>https://dev.to/arshtechpro/nvke-run-your-own-managed-gpu-kubernetes-service-on-bare-metal-ipa</link>
      <guid>https://dev.to/arshtechpro/nvke-run-your-own-managed-gpu-kubernetes-service-on-bare-metal-ipa</guid>
      <description>&lt;p&gt;If you have ever tried to hand out Kubernetes clusters on GPU servers to several teams or customers, you know the problem. Provisioning machines, forming clusters, keeping them isolated, upgrading them and replacing broken nodes all turn into glue scripts very quickly.&lt;/p&gt;

&lt;p&gt;NVIDIA Kubernetes Engine (NVKE) is an open source project that tackles this. It is a management plane you install on your own NVIDIA GPU infrastructure, and it creates and operates Kubernetes clusters on demand. Think of it as the thing that lets a platform team run something like a managed Kubernetes service, but on bare metal that you own.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Heads up:&lt;/strong&gt; NVKE is currently a &lt;strong&gt;preview&lt;/strong&gt;. APIs, configuration and behavior can change between releases, and the project does not recommend it for production workloads. This article covers what that means in practice further down.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You run the NVKE management plane. Your users run workloads on the clusters it creates.&lt;/li&gt;
&lt;li&gt;Every workload cluster gets its own Kubernetes control plane, its own etcd and its own node pools.&lt;/li&gt;
&lt;li&gt;Users talk to their cluster with plain &lt;code&gt;kubectl&lt;/code&gt; and the Kubernetes API. They do not need to know NVKE exists.&lt;/li&gt;
&lt;li&gt;Machines are provisioned through NVIDIA Infra Controller (NICo), which is the only supported infrastructure provider in the preview.&lt;/li&gt;
&lt;li&gt;License is Apache-2.0, and the code is written in Go.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who this is for
&lt;/h2&gt;

&lt;p&gt;NVKE is aimed at &lt;strong&gt;platform teams&lt;/strong&gt;: the people who own a pool of GPU servers and need to give other teams or customers their own isolated Kubernetes clusters.&lt;/p&gt;

&lt;p&gt;If you only want to run workloads on a single GPU cluster, this is probably more than you need. If you are building an internal "Kubernetes as a service" on top of GPU hardware, keep reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it is structured
&lt;/h2&gt;

&lt;p&gt;An NVKE installation has two kinds of Kubernetes cluster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The management cluster.&lt;/strong&gt; This runs the NVKE management plane and the infrastructure controllers. You create it with the &lt;code&gt;nvkectl&lt;/code&gt; command line tool, which uses a temporary seed cluster to bootstrap it. Installing the management plane services is a separate step after that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Workload clusters.&lt;/strong&gt; These are the clusters your users actually run applications on. The management plane creates and operates them through NICo, Cluster API and a node agent.&lt;/p&gt;

&lt;p&gt;The README describes NVKE as a self-hosting factory for Kubernetes clusters, and explicitly says it is not a Kubernetes distribution and not a Cluster API flavour. A throwaway seed cluster stands up the management cluster, and the management plane then provisions and manages many isolated workload clusters.&lt;/p&gt;

&lt;p&gt;Here is the flow in plain terms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;seed cluster (temporary)
        |
        v
management cluster  (runs the NVKE management plane)
        |
        |  requests machines through NICo
        v
workload clusters   (own control plane, etcd, node pools)
        |
        v
your users, using kubectl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What NVKE does for you
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Creates independent workload clusters.&lt;/strong&gt; You request a cluster through the NVKE API or &lt;code&gt;nvkectl&lt;/code&gt;. NVKE provisions the machines through NICo and forms a Kubernetes cluster with its own control plane.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Manages the cluster lifecycle.&lt;/strong&gt; You can create, scale and delete node pools, upgrade Kubernetes and replace nodes. In the preview, some of these steps are manual and have limits, so read the &lt;a href="https://github.com/dsx-ai-factory/nvke/blob/main/docs/workload-clusters/node-pools.md" rel="noopener noreferrer"&gt;node pool guide&lt;/a&gt; before relying on them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Establishes node identity.&lt;/strong&gt; Nodes attest themselves through SPIFFE/SPIRE, using their TPM, before they receive join credentials. This means a machine has to prove what it is before it can join a cluster. Encryption of etcd data is available per cluster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connects builds to deployment.&lt;/strong&gt; A companion project, &lt;a href="https://github.com/dsx-ai-factory/nvkr" rel="noopener noreferrer"&gt;NVIDIA Kubernetes Recipes (NVKR)&lt;/a&gt;, builds the node images and NVKE components from pinned inputs. NVKE deploys those artifacts, while NVKR owns their build and release evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it without GPU hardware
&lt;/h2&gt;

&lt;p&gt;You do not need GPUs or an NVIDIA account to evaluate NVKE. The local quickstart creates a development management plane and a workload cluster on &lt;strong&gt;simulated machines&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;What you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Linux host or VM, amd64 or arm64&lt;/li&gt;
&lt;li&gt;At least 6 CPUs, 12 GiB of free memory and 100 GiB of free disk&lt;/li&gt;
&lt;li&gt;Docker, virtual TPM support and the other tools listed in the guide&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Follow the &lt;a href="https://github.com/dsx-ai-factory/nvke/blob/main/docs/get-started/quickstart-local.md" rel="noopener noreferrer"&gt;local quickstart&lt;/a&gt; through workload cluster creation, and confirm the cluster's nodes report &lt;code&gt;Ready&lt;/code&gt;. That is your success check.&lt;/p&gt;

&lt;p&gt;On macOS, use the &lt;a href="https://github.com/dsx-ai-factory/nvke/blob/main/docs/get-started/local-site.md" rel="noopener noreferrer"&gt;local site guide&lt;/a&gt; instead. It runs the evaluation inside a Colima virtual machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install it on real infrastructure
&lt;/h2&gt;

&lt;p&gt;For a real installation you bring your own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Machines&lt;/li&gt;
&lt;li&gt;Networking&lt;/li&gt;
&lt;li&gt;Node images&lt;/li&gt;
&lt;li&gt;Registries&lt;/li&gt;
&lt;li&gt;Access to NICo&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start with &lt;a href="https://github.com/dsx-ai-factory/nvke/blob/main/docs/operate/plan-installation.md" rel="noopener noreferrer"&gt;Plan your installation&lt;/a&gt;. It covers the prerequisites and the order of operations: prepare nodes, bootstrap the management cluster, then install the management plane services.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/dsx-ai-factory/nvke/blob/main/docs/overview/supported-platforms.md" rel="noopener noreferrer"&gt;platform and version reference&lt;/a&gt; lists the validated combinations and pinned dependencies. Check it before you pick hardware or versions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build from source
&lt;/h2&gt;

&lt;p&gt;If you want to read the code, hack on it or contribute, you can build everything locally.&lt;/p&gt;

&lt;p&gt;Requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go 1.27.3, the version pinned in &lt;code&gt;go.work&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;GNU Make 4 or later&lt;/li&gt;
&lt;li&gt;Public Go modules reachable on the first build&lt;/li&gt;
&lt;li&gt;Optional: &lt;a href="https://flox.dev/" rel="noopener noreferrer"&gt;Flox&lt;/a&gt;, which supplies the repository's development tools (and GNU Make 4 on macOS)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/dsx-ai-factory/nvke.git
&lt;span class="nb"&gt;cd &lt;/span&gt;nvke
make build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This builds the Go workspace and writes seven binaries to &lt;code&gt;bin/&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Binary&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;agent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The node agent, which runs on Linux&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;updater&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nvkectl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The command line tool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;capd-reboot-controller&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nvke-os-image-check&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;management-engine&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Building does not start a management plane or create a cluster. It only produces the binaries.&lt;/p&gt;

&lt;p&gt;The repository is a multi-module Go workspace, and the &lt;a href="https://github.com/dsx-ai-factory/nvke/blob/main/docs/go-workspace.md" rel="noopener noreferrer"&gt;workspace guide&lt;/a&gt; explains how the modules and outputs fit together.&lt;/p&gt;

&lt;h2&gt;
  
  
  What developers should know before starting
&lt;/h2&gt;

&lt;p&gt;This is the section to read twice.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. It is a preview, so expect change
&lt;/h3&gt;

&lt;p&gt;APIs, configuration and behavior can change between releases. Do not plan a production rollout around the current behavior. See &lt;a href="https://github.com/dsx-ai-factory/nvke/blob/main/docs/overview/preview-status.md" rel="noopener noreferrer"&gt;Preview status and known limitations&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. API authentication is off by default
&lt;/h3&gt;

&lt;p&gt;After installation, the API uses plain HTTP and authentication is turned off. You must configure &lt;a href="https://github.com/dsx-ai-factory/nvke/blob/main/docs/operate/api-authentication.md" rel="noopener noreferrer"&gt;API authentication and protected access&lt;/a&gt; before you let any callers in. Do not expose a fresh install to a network you do not control.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The management cluster does not look after itself
&lt;/h3&gt;

&lt;p&gt;The management cluster does not upgrade, scale or replace itself. There is also no management-plane upgrade procedure and no backup and restore procedure yet. Plan for that operational gap.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. FDE node images are for evaluation only
&lt;/h3&gt;

&lt;p&gt;The FDE node images provide no data-at-rest confidentiality. Treat them as an evaluation tool, not a security feature.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. NICo is the only infrastructure provider
&lt;/h3&gt;

&lt;p&gt;For the preview, NVIDIA Infra Controller (NICo) is the sole compatible infrastructure provider. If your hardware is managed some other way, NVKE does not support it yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Some lifecycle steps are manual
&lt;/h3&gt;

&lt;p&gt;Node replacement and Kubernetes upgrades have manual steps and preview limits. The node pool guide documents them.&lt;/p&gt;

&lt;h2&gt;
  
  
  A sensible path for a first evaluation
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Read &lt;a href="https://github.com/dsx-ai-factory/nvke/blob/main/docs/overview/what-is-nvke.md" rel="noopener noreferrer"&gt;What is NVKE&lt;/a&gt; to understand the operator workflow.&lt;/li&gt;
&lt;li&gt;Run the local quickstart on a Linux VM and get a workload cluster to &lt;code&gt;Ready&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Read the &lt;a href="https://github.com/dsx-ai-factory/nvke/blob/main/docs/architecture.md" rel="noopener noreferrer"&gt;Architecture&lt;/a&gt; doc to see the data flows between components.&lt;/li&gt;
&lt;li&gt;Read the known limitations page and decide whether the preview gaps matter for your use case.&lt;/li&gt;
&lt;li&gt;If you have GPU hardware and NICo access, move on to planning a real installation.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Where to find the docs
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;I want to...&lt;/th&gt;
&lt;th&gt;Go to&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Prepare nodes and bootstrap the management cluster&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/dsx-ai-factory/nvke/blob/main/docs/operate/bootstrap-management-cluster.md" rel="noopener noreferrer"&gt;Bootstrap the management cluster&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Install NVKE services&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/dsx-ai-factory/nvke/blob/main/deploy/README.md" rel="noopener noreferrer"&gt;Deploy path README&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build node images and get release artifacts&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/dsx-ai-factory/nvke/blob/main/docs/operate/build-node-images.md" rel="noopener noreferrer"&gt;Get NVKE artifacts from NVKR&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create and manage customer clusters&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/dsx-ai-factory/nvke/blob/main/docs/workload-clusters/create.md" rel="noopener noreferrer"&gt;Create a workload cluster&lt;/a&gt; and &lt;a href="https://github.com/dsx-ai-factory/nvke/blob/main/docs/workload-clusters/node-pools.md" rel="noopener noreferrer"&gt;Manage node pools&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Give a customer access&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/dsx-ai-factory/nvke/blob/main/docs/workload-clusters/access.md" rel="noopener noreferrer"&gt;Access a workload cluster&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inspect cluster health and telemetry&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/dsx-ai-factory/nvke/blob/main/docs/operate/observe.md" rel="noopener noreferrer"&gt;Observe NVKE and its clusters&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The project also publishes documentation at &lt;a href="https://docs.nvidia.com/nvke/" rel="noopener noreferrer"&gt;docs.nvidia.com/nvke&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing up
&lt;/h2&gt;

&lt;p&gt;NVKE gives platform teams a way to run a managed Kubernetes service on their own NVIDIA GPU hardware, with isolated clusters, TPM-based node attestation and a lifecycle managed through an API and &lt;code&gt;nvkectl&lt;/code&gt;. It is early, and the preview limitations are real, especially the disabled API authentication and the missing management-plane upgrade and backup procedures.&lt;/p&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/dsx-ai-factory/nvke" rel="noopener noreferrer"&gt;github.com/dsx-ai-factory/nvke&lt;/a&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>nvidia</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
    <item>
      <title>LZ4 Explained for Developers - The Compression Library Built for Speed</title>
      <dc:creator>ArshTechPro</dc:creator>
      <pubDate>Thu, 05 Nov 2026 12:29:12 +0000</pubDate>
      <link>https://dev.to/arshtechpro/lz4-explained-for-developers-the-compression-library-built-for-speed-1hm</link>
      <guid>https://dev.to/arshtechpro/lz4-explained-for-developers-the-compression-library-built-for-speed-1hm</guid>
      <description>&lt;p&gt;If you have ever used ZFS, zram on Linux, Kafka, or ClickHouse, you have probably used LZ4 without knowing it. It is one of those libraries that quietly sits underneath a lot of infrastructure because it does one thing extremely well: it compresses and decompresses data &lt;em&gt;fast&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This article covers what LZ4 is, when you should (and should not) use it, the things that trip developers up, and how it compares to other free and paid compression tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is LZ4?
&lt;/h2&gt;

&lt;p&gt;LZ4 is a lossless compression algorithm and C library written by Yann Collet (who also created Zstandard). It was first released in 2011, is licensed under the &lt;strong&gt;BSD 2-Clause license&lt;/strong&gt; for the library, and has more than 12k stars on GitHub.&lt;/p&gt;

&lt;p&gt;The design goal is simple: trade some compression ratio for raw speed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compression:&lt;/strong&gt; more than 500 MB/s per core&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decompression:&lt;/strong&gt; multiple GB/s per core, often hitting RAM bandwidth limits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ratio:&lt;/strong&gt; roughly 2:1 on typical mixed data at the default level&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, LZ4 does not make your files as small as gzip or Zstandard can, but it is so fast that compression often costs less than the I/O or network time it saves.&lt;/p&gt;




&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;p&gt;These figures come from the benchmark published in the LZ4 README (lzbench, Silesia corpus, single thread, Core i7-9700K @ 4.9GHz):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Compressor&lt;/th&gt;
&lt;th&gt;Ratio&lt;/th&gt;
&lt;th&gt;Compression&lt;/th&gt;
&lt;th&gt;Decompression&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;memcpy (no compression)&lt;/td&gt;
&lt;td&gt;1.000&lt;/td&gt;
&lt;td&gt;13700 MB/s&lt;/td&gt;
&lt;td&gt;13700 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LZ4 default&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.101&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;780 MB/s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4970 MB/s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LZO 2.09&lt;/td&gt;
&lt;td&gt;2.108&lt;/td&gt;
&lt;td&gt;670 MB/s&lt;/td&gt;
&lt;td&gt;860 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snappy 1.1.4&lt;/td&gt;
&lt;td&gt;2.091&lt;/td&gt;
&lt;td&gt;565 MB/s&lt;/td&gt;
&lt;td&gt;1950 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zstandard 1.4.0 -1&lt;/td&gt;
&lt;td&gt;2.883&lt;/td&gt;
&lt;td&gt;515 MB/s&lt;/td&gt;
&lt;td&gt;1380 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;zlib deflate -1&lt;/td&gt;
&lt;td&gt;2.730&lt;/td&gt;
&lt;td&gt;100 MB/s&lt;/td&gt;
&lt;td&gt;415 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LZ4 HC -9&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.721&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;41 MB/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4900 MB/s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;zlib deflate -6&lt;/td&gt;
&lt;td&gt;3.099&lt;/td&gt;
&lt;td&gt;36 MB/s&lt;/td&gt;
&lt;td&gt;445 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things stand out:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;LZ4 decompresses several times faster than everything else in the table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LZ4 HC&lt;/strong&gt; reaches a ratio close to zlib -1 while keeping the same blazing decompression speed. That matters a lot for "compress once, read many times" workloads.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Note: these numbers are from older versions of each library. Always benchmark with your own data (more on that below).&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Install
&lt;/h3&gt;

&lt;p&gt;Most Linux distributions and package managers already ship it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Debian / Ubuntu&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;lz4 liblz4-dev

&lt;span class="c"&gt;# Fedora / RHEL&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install &lt;/span&gt;lz4 lz4-devel

&lt;span class="c"&gt;# macOS&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;lz4

&lt;span class="c"&gt;# Windows (vcpkg)&lt;/span&gt;
vcpkg &lt;span class="nb"&gt;install &lt;/span&gt;lz4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or build from source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/lz4/lz4.git
&lt;span class="nb"&gt;cd &lt;/span&gt;lz4
make
&lt;span class="nb"&gt;sudo &lt;/span&gt;make &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Command line
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;lz4&lt;/code&gt; CLI feels a lot like &lt;code&gt;gzip&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Compress (creates file.txt.lz4)&lt;/span&gt;
lz4 file.txt

&lt;span class="c"&gt;# Decompress&lt;/span&gt;
lz4 &lt;span class="nt"&gt;-d&lt;/span&gt; file.txt.lz4

&lt;span class="c"&gt;# Higher compression (levels 3-12 use the HC compressor)&lt;/span&gt;
lz4 &lt;span class="nt"&gt;-9&lt;/span&gt; big.log

&lt;span class="c"&gt;# Use multiple threads (v1.10+), 0 = auto-detect cores&lt;/span&gt;
lz4 &lt;span class="nt"&gt;-T0&lt;/span&gt; huge-file.bin

&lt;span class="c"&gt;# Compress a directory with tar&lt;/span&gt;
&lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-I&lt;/span&gt; lz4 &lt;span class="nt"&gt;-cf&lt;/span&gt; backup.tar.lz4 ./my-folder

&lt;span class="c"&gt;# Benchmark levels 1 through 9 on your own file&lt;/span&gt;
lz4 &lt;span class="nt"&gt;-b1e9&lt;/span&gt; sample.dat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last command is the most useful one in this article. Run it on real data before choosing a level.&lt;/p&gt;

&lt;h3&gt;
  
  
  C API
&lt;/h3&gt;

&lt;p&gt;The core API is small. Here is a complete round trip:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;stdio.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;stdlib.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;string.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;lz4.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Hello LZ4! Hello LZ4! Hello LZ4! Hello LZ4!"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;src_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;strlen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cm"&gt;/* Worst-case output size for incompressible input */&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;max_dst&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LZ4_compressBound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src_size&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;compressed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;malloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_dst&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;comp_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LZ4_compress_default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;compressed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;src_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_dst&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;comp_size&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;fprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"compression failed&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="cm"&gt;/* You must know (or store) the original size yourself */&lt;/span&gt;
    &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;restored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;malloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src_size&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;dec_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LZ4_decompress_safe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;compressed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;restored&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;comp_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;src_size&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dec_size&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;fprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"decompression failed&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%d -&amp;gt; %d bytes, restored: %s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;src_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;comp_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;restored&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;free&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;compressed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;free&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;restored&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compile with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcc demo.c &lt;span class="nt"&gt;-llz4&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Python
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;lz4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;lz4.frame&lt;/span&gt;

&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;some repetitive payload &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10_000&lt;/span&gt;

&lt;span class="n"&gt;compressed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;restored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decompress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;compressed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;restored&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;compressed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; bytes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Other languages
&lt;/h3&gt;

&lt;p&gt;LZ4 has bindings or ports in almost every ecosystem:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;Common package&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;lz4&lt;/code&gt; (PyPI)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;&lt;code&gt;github.com/pierrec/lz4/v4&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rust&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;lz4_flex&lt;/code&gt; (pure Rust) or &lt;code&gt;lz4&lt;/code&gt; (bindings)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Java&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;at.yawk.lz4:lz4-java&lt;/code&gt; (see the security note below)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C#/.NET&lt;/td&gt;
&lt;td&gt;&lt;code&gt;K4os.Compression.LZ4&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  What developers really need to know
&lt;/h2&gt;

&lt;p&gt;This is the section to read even if you skip everything else.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Block format vs frame format
&lt;/h3&gt;

&lt;p&gt;LZ4 has two formats, and mixing them up is the most common source of bugs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Block format&lt;/strong&gt; (&lt;code&gt;lz4.h&lt;/code&gt;): raw compressed bytes. No header, no checksum, &lt;strong&gt;no record of the original size&lt;/strong&gt;. You must store the uncompressed size yourself and pass it at decompression time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frame format&lt;/strong&gt; (&lt;code&gt;lz4frame.h&lt;/code&gt;): a self-describing container with a magic number, optional content size, optional xxHash checksums, and support for streaming arbitrarily large data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rule of thumb: &lt;strong&gt;use the frame format&lt;/strong&gt; unless you are building your own container and know exactly why you need raw blocks. The frame format is what the CLI produces, and it is what other tools and languages expect for interoperability.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Always use the safe decompressor
&lt;/h3&gt;

&lt;p&gt;Use &lt;code&gt;LZ4_decompress_safe()&lt;/code&gt; (or the frame API). It checks bounds and will not write past your buffer even if the input is corrupted or malicious.&lt;/p&gt;

&lt;p&gt;Avoid &lt;code&gt;LZ4_decompress_fast()&lt;/code&gt;. It is deprecated, and it trusts the input, which makes it unsafe for anything that comes from a network, a user, or a disk you do not fully control.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Compression level does not slow down decompression
&lt;/h3&gt;

&lt;p&gt;LZ4 offers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Acceleration&lt;/strong&gt; (&lt;code&gt;LZ4_compress_fast&lt;/code&gt; with a factor &amp;gt; 1): even faster compression, lower ratio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Default&lt;/strong&gt; (level 1): the classic fast mode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Level 2&lt;/strong&gt;: new in v1.10, a middle ground between fast and HC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HC levels 3 to 12&lt;/strong&gt;: much slower compression, better ratio.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of these decompress at essentially the same speed. So for assets, packages, or anything written once and read many times, use a high HC level at build time and enjoy fast reads forever.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Small data needs a dictionary
&lt;/h3&gt;

&lt;p&gt;Compressing a 300-byte JSON message on its own gives poor results because there is no history to find repeats in. LZ4 supports &lt;strong&gt;dictionary compression&lt;/strong&gt;, which pre-loads shared context. Only the last 64 KB of a dictionary is used.&lt;/p&gt;

&lt;p&gt;You can train a dictionary with Zstandard's builder and use it with LZ4:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;zstd &lt;span class="nt"&gt;--train&lt;/span&gt; samples/&lt;span class="k"&gt;*&lt;/span&gt;.json &lt;span class="nt"&gt;-o&lt;/span&gt; messages.dict
lz4 &lt;span class="nt"&gt;-D&lt;/span&gt; messages.dict message.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dictionary support was fully stabilized in v1.10.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Multithreading arrived in v1.10
&lt;/h3&gt;

&lt;p&gt;Version 1.10.0 (July 2024, "Multicores edition") added multithreaded compression to the CLI, which gives large speedups on big files with many cores. Decompression is still effectively single-threaded, but it is already fast enough to saturate most NVMe drives.&lt;/p&gt;

&lt;p&gt;If your distro ships an older 1.9.x build, you will not get &lt;code&gt;-T&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Do not compress data that is already compressed
&lt;/h3&gt;

&lt;p&gt;JPEGs, MP4s, ZIPs, and encrypted data will not shrink. LZ4 is fast enough that this is rarely a disaster, but you still burn CPU for nothing. Many systems check the ratio after compressing and store the raw data if the gain is too small.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Check your bindings for security updates
&lt;/h3&gt;

&lt;p&gt;The C library is heavily fuzzed (it is part of OSS-Fuzz), but bindings are separate projects with their own bugs. A notable example: the original &lt;code&gt;org.lz4:lz4-java&lt;/code&gt; is no longer maintained, and the community fork moved to the &lt;code&gt;at.yawk.lz4:lz4-java&lt;/code&gt; Maven coordinates, which includes fixes for CVE-2025-12183 and CVE-2025-66566. If you are on the JVM, check which artifact your dependency tree actually pulls in (Kafka clients and many other libraries depend on it transitively).&lt;/p&gt;




&lt;h2&gt;
  
  
  Where LZ4 is used in the real world
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Linux kernel&lt;/strong&gt;: zram, zswap, SquashFS, and kernel image compression&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filesystems&lt;/strong&gt;: ZFS and OpenZFS use LZ4 as the go-to on-the-fly compression&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Databases and analytics&lt;/strong&gt;: ClickHouse (default codec), plus many column stores&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Messaging&lt;/strong&gt;: Kafka and Pulsar support LZ4 for message batches&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Game engines and asset pipelines&lt;/strong&gt;: fast asset loading&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The common thread: places where latency and throughput matter more than squeezing out the last few percent of disk space.&lt;/p&gt;




&lt;h2&gt;
  
  
  Comparison with other tools
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Free and open source
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;License&lt;/th&gt;
&lt;th&gt;Speed&lt;/th&gt;
&lt;th&gt;Ratio&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LZ4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;BSD-2&lt;/td&gt;
&lt;td&gt;Fastest decompression, very fast compression&lt;/td&gt;
&lt;td&gt;Low to medium (HC)&lt;/td&gt;
&lt;td&gt;Real-time, in-memory, storage engines, read-heavy data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Zstandard (zstd)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;BSD / GPLv2&lt;/td&gt;
&lt;td&gt;Fast, tunable from very fast to very slow&lt;/td&gt;
&lt;td&gt;Medium to very high&lt;/td&gt;
&lt;td&gt;The best general-purpose default today&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Snappy&lt;/strong&gt; (Google)&lt;/td&gt;
&lt;td&gt;BSD-3&lt;/td&gt;
&lt;td&gt;Fast&lt;/td&gt;
&lt;td&gt;Low (similar to LZ4)&lt;/td&gt;
&lt;td&gt;Existing Google/Hadoop/Parquet ecosystems&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;gzip / zlib (deflate)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;zlib&lt;/td&gt;
&lt;td&gt;Slow compression, slow decompression&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Maximum compatibility (HTTP, archives, everywhere)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Brotli&lt;/strong&gt; (Google)&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Slow at high levels, decent decompression&lt;/td&gt;
&lt;td&gt;High, especially for text&lt;/td&gt;
&lt;td&gt;Static web assets served over HTTP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;xz / LZMA&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Public domain / LGPL&lt;/td&gt;
&lt;td&gt;Very slow compression&lt;/td&gt;
&lt;td&gt;Very high&lt;/td&gt;
&lt;td&gt;Distribution packages, long-term archives&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LZO&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;GPL&lt;/td&gt;
&lt;td&gt;Fast&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Legacy systems&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;How to read this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LZ4 vs Zstandard&lt;/strong&gt;: Zstandard (same author) is the better all-rounder. At its fastest levels it is competitive with LZ4 on compression speed and gets a noticeably better ratio, but LZ4 still decompresses roughly 2-4x faster. Pick LZ4 when decompression speed or CPU budget is the bottleneck; pick zstd when storage or bandwidth costs dominate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LZ4 vs Snappy&lt;/strong&gt;: they target the same niche with similar ratios. In most benchmarks LZ4 is faster on both compression and decompression. Snappy is mainly worth choosing when your ecosystem already standardizes on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LZ4 vs gzip&lt;/strong&gt;: gzip compresses better but is many times slower in both directions. Use gzip when you need universal compatibility, not speed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LZ4 vs Brotli / xz&lt;/strong&gt;: different goals. Those tools aim for small output and accept slow compression; LZ4 aims for speed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Paid and commercial options
&lt;/h3&gt;

&lt;p&gt;There are few commercial libraries in this space because the open source options are so strong, but a few are worth knowing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;What it offers&lt;/th&gt;
&lt;th&gt;When it makes sense&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Oodle&lt;/strong&gt; (Epic Games Tools, formerly RAD Game Tools)&lt;/td&gt;
&lt;td&gt;Proprietary, commercially licensed; bundled with Unreal Engine&lt;/td&gt;
&lt;td&gt;Codecs such as Kraken, Mermaid, Selkie and Leviathan, tuned for very fast decompression with better ratios than LZ4 at similar speeds; also network packet compression&lt;/td&gt;
&lt;td&gt;Game studios shipping large assets, especially on consoles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Intel QuickAssist Technology (QAT)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hardware accelerator, paid via supported CPUs or add-in cards&lt;/td&gt;
&lt;td&gt;Offloads compression (including deflate and LZ4) from the CPU&lt;/td&gt;
&lt;td&gt;High-throughput servers where CPU cycles are expensive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Commercial archivers&lt;/strong&gt; (WinZip, WinRAR, PKZIP)&lt;/td&gt;
&lt;td&gt;Paid end-user software&lt;/td&gt;
&lt;td&gt;GUI tools, encryption, archive management&lt;/td&gt;
&lt;td&gt;Desktop users, not libraries you embed in an application&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Honest take: for the vast majority of developers there is no compelling reason to pay for a compression library. The paid options shine in narrow cases, mainly game asset delivery (Oodle) and hardware offload at data center scale (QAT). For everything else, LZ4 and Zstandard cover the field.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;LZ4 is not trying to win the compression ratio contest. It is trying to make compression so cheap that you can turn it on almost everywhere, and it succeeds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Further reading
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/lz4/lz4" rel="noopener noreferrer"&gt;LZ4 on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>performance</category>
      <category>c</category>
      <category>programming</category>
      <category>opensource</category>
    </item>
    <item>
      <title>T3 Code: A Free, Open-Source Control Panel for Claude Code, Codex and Other Coding Agents</title>
      <dc:creator>ArshTechPro</dc:creator>
      <pubDate>Tue, 03 Nov 2026 07:19:51 +0000</pubDate>
      <link>https://dev.to/arshtechpro/t3-code-a-free-open-source-control-panel-for-claude-code-codex-and-other-coding-agents-1p10</link>
      <guid>https://dev.to/arshtechpro/t3-code-a-free-open-source-control-panel-for-claude-code-codex-and-other-coding-agents-1p10</guid>
      <description>&lt;p&gt;If you use AI coding agents every day, you have probably hit this wall: three terminals running Claude Code, one running Codex, all poking at the same repo, and no single place to see what each one is doing or what it changed.&lt;/p&gt;

&lt;p&gt;T3 Code is an attempt to fix that. It comes from Theo Browne's team (the people behind the T3 Stack and T3 Chat), it is MIT licensed, and it costs nothing. In this post I will cover what it actually is, how to get it running, what you need to watch out for, and how it stacks up against the free and paid alternatives.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;T3 Code is &lt;strong&gt;not&lt;/strong&gt; a coding agent and has &lt;strong&gt;no model of its own&lt;/strong&gt;. It is a GUI and control layer that drives the agent CLIs you already have installed.&lt;/li&gt;
&lt;li&gt;It works with &lt;strong&gt;Claude Code, Codex, Cursor, Grok Build, OpenCode and Google Antigravity&lt;/strong&gt;, using your existing subscriptions.&lt;/li&gt;
&lt;li&gt;You get a desktop app (macOS, Windows, Linux), a web app, and iOS/Android apps for checking on agents remotely.&lt;/li&gt;
&lt;li&gt;Each task is a &lt;strong&gt;thread&lt;/strong&gt; that can run on its own Git branch or worktree, with an inline diff and a one-click PR flow.&lt;/li&gt;
&lt;li&gt;It is &lt;strong&gt;very early software&lt;/strong&gt; (v0.0.x). New threads default to &lt;strong&gt;Full access&lt;/strong&gt;, which means no approval prompts. Change that first.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What T3 Code actually is
&lt;/h2&gt;

&lt;p&gt;The project describes itself as an "agent harness control surface". In plain terms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; You (desktop / web / phone)
          |
          v
   T3 Code server  &amp;lt;-- owns processes, terminals, Git, project files
          |
   +------+-------+-----------+----------+
   |      |       |           |          |
 Claude  Codex  Cursor CLI  OpenCode   Grok / Antigravity
  Code    CLI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A server process on your machine owns everything that runs: the agent processes, terminals, Git operations and project files. The desktop, web and mobile apps are thin clients that connect to that server. The desktop app bundles its own server, so on a single machine you do not have to think about this split at all.&lt;/p&gt;

&lt;p&gt;Because T3 Code drives the official CLIs, it signs in through each CLI's own login. It does not resell API keys or add its own usage quota. Whatever you pay Anthropic, OpenAI or Cursor today is what you keep paying. T3 Code itself is $0.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why developers are paying attention
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;One window for many agents.&lt;/strong&gt; Run Claude Code and Codex side by side, compare their approaches on the same problem, and switch models in the middle of a thread.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Branch per thread.&lt;/strong&gt; Each thread can work on its own branch or Git worktree. A worktree is a second checkout of the same repo on a different branch, so two agents can edit files at the same time without stepping on each other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Diff review and PRs in the same place.&lt;/strong&gt; You see the diff as the agent works, then commit, push and open a pull request from the app. The PR gets a generated title and body.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remote control.&lt;/strong&gt; Leave the agents running on your workstation and check on them from your phone or laptop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An MCP server.&lt;/strong&gt; Other agents (Claude Code in a terminal, Codex, ChatGPT) can connect to T3 Code over MCP to read threads and start new ones. That makes a "coordinator agent" pattern possible without writing your own plumbing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Install and log in to at least one agent CLI
&lt;/h3&gt;

&lt;p&gt;T3 Code cannot do anything without an agent behind it. Set up at least one of these first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Claude Code&lt;/span&gt;
claude auth login

&lt;span class="c"&gt;# Codex&lt;/span&gt;
codex login

&lt;span class="c"&gt;# Cursor CLI&lt;/span&gt;
agent login

&lt;span class="c"&gt;# OpenCode&lt;/span&gt;
opencode auth login

&lt;span class="c"&gt;# Grok Build&lt;/span&gt;
grok login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Google Antigravity is the exception: you enable it inside T3 Code's settings and sign in with Google. No CLI needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Install T3 Code
&lt;/h3&gt;

&lt;p&gt;Pick whichever fits your workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it once without installing anything permanent:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx t3@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Install the CLI (server plus local web app):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# macOS / Linux&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://t3.codes/install.sh | sh

&lt;span class="c"&gt;# Windows (PowerShell)&lt;/span&gt;
irm https://t3.codes/install.ps1 | iex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;t3                   &lt;span class="c"&gt;# start the server and open the local web app&lt;/span&gt;
t3 service &lt;span class="nb"&gt;install&lt;/span&gt;   &lt;span class="c"&gt;# keep it running in the background&lt;/span&gt;
t3 update            &lt;span class="c"&gt;# move to a newer release&lt;/span&gt;
t3 &lt;span class="nt"&gt;--help&lt;/span&gt;            &lt;span class="c"&gt;# full reference&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Install the desktop app:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# macOS&lt;/span&gt;
brew &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--cask&lt;/span&gt; t3-code

&lt;span class="c"&gt;# Windows&lt;/span&gt;
winget &lt;span class="nb"&gt;install &lt;/span&gt;T3Tools.T3Code
&lt;span class="c"&gt;# or&lt;/span&gt;
scoop &lt;span class="nb"&gt;install &lt;/span&gt;extras/t3code

&lt;span class="c"&gt;# Debian / Ubuntu (download the .deb from GitHub Releases first)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; ./T3-Code-&lt;span class="k"&gt;*&lt;/span&gt;.deb

&lt;span class="c"&gt;# Arch Linux&lt;/span&gt;
yay &lt;span class="nt"&gt;-S&lt;/span&gt; t3code-bin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Change the default permission mode
&lt;/h3&gt;

&lt;p&gt;Do this before your first real task. See the next section for why.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Open a project and start a thread
&lt;/h3&gt;

&lt;p&gt;Add your repo as a project, start a thread, choose the agent and whether it should run in a new worktree, and give it a task. When it finishes, review the diff and decide whether to open a PR or throw the branch away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Permission modes: the setting that matters most
&lt;/h2&gt;

&lt;p&gt;Every thread has a permission mode that decides how much the agent can do without asking you:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;When to use it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Supervised&lt;/td&gt;
&lt;td&gt;Asks before commands and file changes. Work outside the workspace is restricted.&lt;/td&gt;
&lt;td&gt;Important repos, unfamiliar tasks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-accept edits&lt;/td&gt;
&lt;td&gt;File edits go through, commands still need approval&lt;/td&gt;
&lt;td&gt;Refactors where you mainly care about shell commands&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto&lt;/td&gt;
&lt;td&gt;Routine actions run, risky ones still ask&lt;/td&gt;
&lt;td&gt;Day-to-day work with providers that support it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full access&lt;/td&gt;
&lt;td&gt;No prompts at all for commands or edits&lt;/td&gt;
&lt;td&gt;Throwaway worktrees and sandboxes only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things to know:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Full access is the default for new threads.&lt;/strong&gt; The agent can run any command and edit any file without asking. For a tool designed to run several agents at once, that is a risky default. Change it in Settings to Supervised or Auto.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto mode depends on the provider.&lt;/strong&gt; Codex hands routine approvals to an AI reviewer and Claude uses its own auto permission mode. Providers without an equivalent, such as OpenCode, fall back to asking every time, like Supervised.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The mode is set per thread, so changing it in one thread does not affect others.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remote access in practice
&lt;/h2&gt;

&lt;p&gt;The machine running T3 Code does the work. Your phone or laptop is just a remote. That means the host has to stay awake and reachable.&lt;/p&gt;

&lt;p&gt;You have three main ways to connect:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LAN or Tailscale (traffic stays on your network):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;t3 serve &lt;span class="nt"&gt;--host&lt;/span&gt; &amp;lt;your-lan-or-tailnet-ip&amp;gt;
t3 pair            &lt;span class="c"&gt;# prints a QR code and a one-time pairing link&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For HTTPS over Tailscale:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;t3 serve &lt;span class="nt"&gt;--tailscale-serve&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;T3 Connect (no router or VPN setup):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;t3 connect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This needs a T3 account and routes the connection through T3's relay. Convenient, but be aware of that trade-off if your company has rules about where code-related traffic goes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SSH from the desktop app:&lt;/strong&gt; add an SSH host in Settings, and T3 Code starts a server there and forwards the port for you. Projects, credentials and agent work stay on the remote machine. The remote host must be Linux or an Apple Silicon Mac.&lt;/p&gt;

&lt;p&gt;Treat pairing links and codes like passwords. Keep them out of screenshots, logs and bug reports.&lt;/p&gt;

&lt;h2&gt;
  
  
  Letting other agents drive T3 Code (MCP)
&lt;/h2&gt;

&lt;p&gt;Every T3 Code environment exposes an MCP endpoint at &lt;code&gt;https://&amp;lt;environment-address&amp;gt;/mcp&lt;/code&gt;. For example, to connect Claude Code running in a terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add &lt;span class="nt"&gt;--transport&lt;/span&gt; http t3 https://&amp;lt;environment-address&amp;gt;/mcp
claude mcp login t3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Codex:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;codex mcp add t3 &lt;span class="nt"&gt;--url&lt;/span&gt; https://&amp;lt;environment-address&amp;gt;/mcp
codex mcp login t3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A new agent gets &lt;strong&gt;Read only&lt;/strong&gt; access by default. You can raise it to Supervised or higher so it can start, message and stop threads. The address must be HTTPS (or &lt;code&gt;localhost&lt;/code&gt; on the same machine), and sign-ins expire after 30 days.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to know before you adopt it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;It is early.&lt;/strong&gt; The README says to expect bugs, and stable releases are still on v0.0.x with nightly builds shipping several times a day. Stick to the stable channel for real work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contributions are mostly closed.&lt;/strong&gt; The team says small fixes may be considered, but big features will not be accepted for now. Feature ideas go to GitHub Discussions. If you need something specific, the MIT license lets you fork.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Docs live in the repo.&lt;/strong&gt; There is no docs site yet. Everything is under &lt;a href="https://github.com/pingdotgg/t3code/tree/main/docs" rel="noopener noreferrer"&gt;&lt;code&gt;docs/&lt;/code&gt;&lt;/a&gt;, and it is genuinely useful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your existing agent config still applies.&lt;/strong&gt; Because T3 Code runs the real CLIs, things like your &lt;code&gt;CLAUDE.md&lt;/code&gt;, &lt;code&gt;AGENTS.md&lt;/code&gt;, hooks and MCP servers configured for each CLI should keep working.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The PR flow assumes a Git and GitHub-style workflow.&lt;/strong&gt; If your work is not branch-and-PR shaped, you will get less out of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Costs still exist, just not here.&lt;/strong&gt; Running four agents in parallel burns through your Claude or ChatGPT plan limits four times faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  How T3 Code compares to other tools
&lt;/h2&gt;

&lt;p&gt;There are two broad groups to compare against: &lt;strong&gt;orchestrators&lt;/strong&gt; (tools like T3 Code that sit on top of agents) and &lt;strong&gt;AI editors and first-party agent apps&lt;/strong&gt; (tools that come with their own model access).&lt;/p&gt;

&lt;h3&gt;
  
  
  Orchestrators and agent managers
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;License&lt;/th&gt;
&lt;th&gt;Platforms&lt;/th&gt;
&lt;th&gt;Agents supported&lt;/th&gt;
&lt;th&gt;Interface&lt;/th&gt;
&lt;th&gt;Mobile / remote&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;T3 Code&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;macOS, Windows, Linux, web&lt;/td&gt;
&lt;td&gt;Claude Code, Codex, Cursor, OpenCode, Grok Build, Antigravity&lt;/td&gt;
&lt;td&gt;Desktop + web GUI&lt;/td&gt;
&lt;td&gt;Yes: iOS, Android, web&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Conductor&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Free local plan; paid Pro for cloud workspaces&lt;/td&gt;
&lt;td&gt;Closed source&lt;/td&gt;
&lt;td&gt;macOS only&lt;/td&gt;
&lt;td&gt;Claude Code, Codex, Cursor, OpenCode&lt;/td&gt;
&lt;td&gt;Native Mac app&lt;/td&gt;
&lt;td&gt;Cloud workspaces on paid plan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Claude Squad&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;AGPL-3.0&lt;/td&gt;
&lt;td&gt;macOS, Linux (Windows via WSL)&lt;/td&gt;
&lt;td&gt;Claude Code, Codex, Gemini, Aider, others&lt;/td&gt;
&lt;td&gt;Terminal UI (tmux)&lt;/td&gt;
&lt;td&gt;Via SSH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vibe Kanban&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;td&gt;Cross-platform (runs via npx)&lt;/td&gt;
&lt;td&gt;10+ including Claude Code, Codex, Gemini CLI, Copilot, Amp, Cursor, OpenCode&lt;/td&gt;
&lt;td&gt;Kanban board in browser&lt;/td&gt;
&lt;td&gt;Self-host and tunnel&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Quick notes on each:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Conductor&lt;/strong&gt; is the closest competitor and the most polished Mac experience. It uses Git worktrees per workspace, like T3 Code. Its free plan covers local workspaces; reviews list Pro at around $50/month for cloud workspaces that keep running when your laptop is closed. If you are not on a Mac, it is not an option.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Squad&lt;/strong&gt; is a lightweight Go binary built on tmux and worktrees. It is great if you live in the terminal and do not want a GUI at all. No phone app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vibe Kanban&lt;/strong&gt; frames agent work as kanban tickets, which suits people who like to plan a backlog first. Its original company, bloop, shut down in 2026 and the project continues as a community-maintained open-source tool, which is worth weighing for long-term support.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  AI editors and first-party agent apps
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;What you get&lt;/th&gt;
&lt;th&gt;Difference from T3 Code&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cursor&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Free Hobby tier; Pro $20/mo, Pro+ $60/mo, Ultra $200/mo, Teams $40/user/mo&lt;/td&gt;
&lt;td&gt;Full AI code editor with bundled model usage&lt;/td&gt;
&lt;td&gt;It is an editor with its own billing. T3 Code is a control layer and can even drive the Cursor CLI.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Codex app&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Included with ChatGPT plans&lt;/td&gt;
&lt;td&gt;OpenAI's own app for Codex&lt;/td&gt;
&lt;td&gt;Codex only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Claude Code / Claude desktop&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Included with Claude plans&lt;/td&gt;
&lt;td&gt;Anthropic's own agent, with built-in worktree support&lt;/td&gt;
&lt;td&gt;Claude only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key difference: Cursor, Codex and Claude Code are where the model access and billing live. T3 Code does not replace them. It sits on top and lets you run several of them from one place.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which one should you pick?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You use more than one agent and want a GUI on any OS:&lt;/strong&gt; T3 Code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You are on a Mac, want the most polished experience, and are fine with closed source:&lt;/strong&gt; Conductor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You prefer staying entirely in the terminal:&lt;/strong&gt; Claude Squad.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want to plan agent work as tickets on a board:&lt;/strong&gt; Vibe Kanban.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You only use one agent and are happy with it:&lt;/strong&gt; Its own app or CLI is probably enough. Claude Code and Codex both support parallel work on their own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want an editor with AI built in, not an agent manager:&lt;/strong&gt; Cursor.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;T3 Code is not trying to be another AI model or another editor. It is a free, open control layer for the agents you already pay for, with worktrees, diffs, PRs and a phone app in one place. It is rough around the edges and changing fast, so treat it as early-adopter software and lock down the permission defaults. But if you already juggle several coding agents, it is one of the more interesting open-source projects to try right now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/pingdotgg/t3code" rel="noopener noreferrer"&gt;pingdotgg/t3code&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Pricing and feature details were checked in early November 2026. These tools change quickly, so verify on each project's site before making a decision.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>productivity</category>
      <category>git</category>
    </item>
    <item>
      <title>Maestro Deep Dive: Testing a SwiftUI App End to End</title>
      <dc:creator>ArshTechPro</dc:creator>
      <pubDate>Tue, 27 Oct 2026 19:13:41 +0000</pubDate>
      <link>https://dev.to/arshtechpro/maestro-deep-dive-testing-a-swiftui-app-end-to-end-with-ci-3bkp</link>
      <guid>https://dev.to/arshtechpro/maestro-deep-dive-testing-a-swiftui-app-end-to-end-with-ci-3bkp</guid>
      <description>&lt;p&gt;In Part 1 we covered what Maestro is, how it works and how it compares to Appium, XCUITest, Detox and the paid device clouds. Now it is time to write real tests.&lt;/p&gt;

&lt;p&gt;In this article we will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build a small SwiftUI app with a login screen and a to-do list.&lt;/li&gt;
&lt;li&gt;Make it testable with accessibility identifiers.&lt;/li&gt;
&lt;li&gt;Write a Maestro suite with reusable sub-flows, environment variables, conditional logic, JavaScript and tags.&lt;/li&gt;
&lt;li&gt;Run it locally, generate reports and debug failures.&lt;/li&gt;
&lt;li&gt;Look at how the same test would look in XCUITest, and where AI agents fit in.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything here uses the free, open-source Maestro CLI. The full app is about 120 lines of Swift, so you can copy it into a new Xcode project and follow along.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;macOS with Xcode 16 or newer and an iOS 17+ Simulator runtime&lt;/li&gt;
&lt;li&gt;Java 17 or newer (&lt;code&gt;java -version&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Maestro CLI:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; &lt;span class="s2"&gt;"https://get.maestro.mobile.dev"&lt;/span&gt; | bash
maestro &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This article was written against Maestro CLI 2.10.0.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part A: The sample app
&lt;/h2&gt;

&lt;p&gt;Create a new Xcode project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Template: &lt;strong&gt;iOS &amp;gt; App&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Product name: &lt;code&gt;TodoDemo&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Organization identifier: &lt;code&gt;com.example&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Interface: &lt;strong&gt;SwiftUI&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Minimum deployment: iOS 17&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That gives the bundle ID &lt;code&gt;com.example.TodoDemo&lt;/code&gt;, which Maestro uses as the &lt;code&gt;appId&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The app has two screens:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Login&lt;/strong&gt;: email, password, a "Log In" button and an error message for wrong credentials. The valid credentials are &lt;code&gt;demo@example.com&lt;/code&gt; / &lt;code&gt;password123&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My Tasks&lt;/strong&gt;: add tasks, tap to complete them, swipe to delete, see a progress label, and log out. After login it asks for notification permission, which gives us a system dialog to deal with.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;TodoDemoApp.swift&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;SwiftUI&lt;/span&gt;

&lt;span class="kd"&gt;@main&lt;/span&gt;
&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;TodoDemoApp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;App&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;@AppStorage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"isLoggedIn"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;isLoggedIn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;some&lt;/span&gt; &lt;span class="kt"&gt;Scene&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;WindowGroup&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;isLoggedIn&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="kt"&gt;TodoListView&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="kt"&gt;LoginView&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The login state is stored in &lt;code&gt;UserDefaults&lt;/code&gt; through &lt;code&gt;@AppStorage&lt;/code&gt;. That matters for testing: it survives an app restart, so we will need to clear app state between tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;LoginView.swift&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;SwiftUI&lt;/span&gt;

&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;LoginView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;@AppStorage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"isLoggedIn"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;isLoggedIn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="kd"&gt;@State&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
    &lt;span class="kd"&gt;@State&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
    &lt;span class="kd"&gt;@State&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;errorMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;some&lt;/span&gt; &lt;span class="kt"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;NavigationStack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;Form&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="kt"&gt;Section&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="kt"&gt;TextField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;$email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keyboardType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;emailAddress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;textInputAutocapitalization&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;never&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;autocorrectionDisabled&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;accessibilityIdentifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"emailField"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                    &lt;span class="kt"&gt;SecureField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Password"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;$password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;accessibilityIdentifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"passwordField"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;

                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;errorMessage&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="kt"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errorMessage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;foregroundStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;red&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;accessibilityIdentifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"loginError"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;

                &lt;span class="kt"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Log In"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;logIn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;accessibilityIdentifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"loginButton"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isEmpty&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isEmpty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;navigationTitle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Welcome"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;logIn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Demo only. A real app would call an auth service here.&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lowercased&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"demo@example.com"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"password123"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;errorMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;
            &lt;span class="n"&gt;isLoggedIn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;errorMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Invalid email or password"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;TodoListView.swift&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;SwiftUI&lt;/span&gt;
&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;UserNotifications&lt;/span&gt;

&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;TodoItem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Identifiable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;isDone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;TodoListView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;@AppStorage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"isLoggedIn"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;isLoggedIn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="kd"&gt;@State&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;TodoItem&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="kd"&gt;@State&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;newTitle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;doneCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(\&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isDone&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;some&lt;/span&gt; &lt;span class="kt"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;NavigationStack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;VStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;spacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="kt"&gt;HStack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="kt"&gt;TextField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"New task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;$newTitle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;textFieldStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;roundedBorder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;addItem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;accessibilityIdentifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"newTaskField"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                    &lt;span class="kt"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Add"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;addItem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;accessibilityIdentifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"addTaskButton"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newTitle&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trimmingCharacters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;whitespaces&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isEmpty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;horizontal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isEmpty&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="kt"&gt;ContentUnavailableView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="s"&gt;"No tasks yet"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="nv"&gt;systemImage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"checklist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="nv"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Add your first task above."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="kt"&gt;List&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="kt"&gt;ForEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;$items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;$item&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
                            &lt;span class="kt"&gt;Button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                                &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isDone&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nv"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                                &lt;span class="kt"&gt;HStack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                                    &lt;span class="kt"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;systemName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isDone&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s"&gt;"checkmark.circle.fill"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"circle"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                                    &lt;span class="kt"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strikethrough&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isDone&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                                &lt;span class="p"&gt;}&lt;/span&gt;
                            &lt;span class="p"&gt;}&lt;/span&gt;
                        &lt;span class="p"&gt;}&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;onDelete&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;atOffsets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;

                    &lt;span class="kt"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;doneCount&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt; of &lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt; done"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;font&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;footnote&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;foregroundStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;secondary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;accessibilityIdentifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"progressLabel"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;navigationTitle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"My Tasks"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;toolbar&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="kt"&gt;ToolbarItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;placement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;topBarTrailing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="kt"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Log Out"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;isLoggedIn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;accessibilityIdentifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"logoutButton"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="kt"&gt;UNUserNotificationCenter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestAuthorization&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sound&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;addItem&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;newTitle&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trimmingCharacters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;whitespaces&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isEmpty&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;TodoItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;newTitle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it once in the Simulator from Xcode to make sure it works.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why the accessibility identifiers?
&lt;/h3&gt;

&lt;p&gt;Maestro can find elements by visible text (&lt;code&gt;tapOn: "Log In"&lt;/code&gt;), and that is often fine. But text changes: a designer renames "Log In" to "Sign In", or you test in German. Identifiers do not change with copy or locale.&lt;/p&gt;

&lt;p&gt;A practical rule:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Static controls&lt;/strong&gt; (buttons, fields, labels you assert on): give them an &lt;code&gt;accessibilityIdentifier&lt;/code&gt; and target them with &lt;code&gt;id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic content&lt;/strong&gt; (task titles, user names, server data): target by text, because the text is the thing you are verifying.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can see exactly what Maestro sees with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;maestro hierarchy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or by opening the Maestro Studio desktop app and clicking on elements in the live device view.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part B: Build and install for the Simulator
&lt;/h2&gt;

&lt;p&gt;Maestro tests the &lt;strong&gt;Simulator build&lt;/strong&gt; of your app (&lt;code&gt;.app&lt;/code&gt;). You can install it by running from Xcode, or from the command line, which is what CI will do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Boot a simulator (the device name depends on your installed runtimes)&lt;/span&gt;
xcrun simctl boot &lt;span class="s2"&gt;"iPhone 16"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;

&lt;span class="c"&gt;# Build for the simulator&lt;/span&gt;
xcodebuild &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-project&lt;/span&gt; TodoDemo.xcodeproj &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-scheme&lt;/span&gt; TodoDemo &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-configuration&lt;/span&gt; Debug &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-sdk&lt;/span&gt; iphonesimulator &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-destination&lt;/span&gt; &lt;span class="s1"&gt;'platform=iOS Simulator,name=iPhone 16'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-derivedDataPath&lt;/span&gt; build &lt;span class="se"&gt;\&lt;/span&gt;
  build

&lt;span class="c"&gt;# Install the .app on the booted simulator&lt;/span&gt;
xcrun simctl &lt;span class="nb"&gt;install &lt;/span&gt;booted build/Build/Products/Debug-iphonesimulator/TodoDemo.app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Part C: The test suite
&lt;/h2&gt;

&lt;p&gt;Here is the structure we are going to build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.maestro/
├── config.yaml
├── subflows/
│   ├── login.yaml
│   └── dismiss_permission_dialog.yaml
└── flows/
    ├── 01_login_invalid.yaml
    ├── 02_login_success.yaml
    ├── 03_add_and_complete_tasks.yaml
    ├── 04_delete_task.yaml
    └── 05_session_and_logout.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;flows/&lt;/code&gt; contains the tests. &lt;code&gt;subflows/&lt;/code&gt; contains reusable building blocks that are never run on their own.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;config.yaml&lt;/code&gt;: the workspace configuration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Only files matching these patterns are treated as tests.&lt;/span&gt;
&lt;span class="c1"&gt;# Sub-flows live outside flows/, so they are not run directly.&lt;/span&gt;
&lt;span class="na"&gt;flows&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flows/*"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you run &lt;code&gt;maestro test .maestro/&lt;/code&gt;, Maestro reads this file to decide which flows belong to the suite.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sub-flow 1: &lt;code&gt;login.yaml&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;com.example.TodoDemo&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;emailField"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;inputText&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${EMAIL || 'demo@example.com'}&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;passwordField"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;inputText&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${PASSWORD || 'password123'}&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;loginButton"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to notice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Everything inside &lt;code&gt;${...}&lt;/code&gt; is JavaScript. &lt;code&gt;EMAIL || 'demo@example.com'&lt;/code&gt; means "use the &lt;code&gt;EMAIL&lt;/code&gt; variable if someone passed one, otherwise fall back to the default". That lets the same sub-flow handle valid and invalid logins.&lt;/li&gt;
&lt;li&gt;There are no waits. After &lt;code&gt;tapOn: { id: "emailField" }&lt;/code&gt;, Maestro waits for the screen to settle before typing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Sub-flow 2: &lt;code&gt;dismiss_permission_dialog.yaml&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;com.example.TodoDemo&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;runFlow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;visible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Allow"&lt;/span&gt;
    &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Allow"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a &lt;strong&gt;conditional flow&lt;/strong&gt;. If the system notification dialog is on screen, tap "Allow". If it is not, do nothing and continue. Because Maestro drives the whole device rather than just your app process, it can tap system alerts that in-process frameworks cannot reach directly.&lt;/p&gt;

&lt;p&gt;We will mostly avoid the dialog entirely by pre-granting permissions in &lt;code&gt;launchApp&lt;/code&gt;, but this sub-flow is a safety net for cases where you cannot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test 1: &lt;code&gt;01_login_invalid.yaml&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;com.example.TodoDemo&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Login fails with wrong credentials&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;auth&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;regression&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;launchApp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;clearState&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;assertVisible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Welcome"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;runFlow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;../subflows/login.yaml&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;EMAIL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wrong@example.com"&lt;/span&gt;
      &lt;span class="na"&gt;PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not-the-password"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;assertVisible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;loginError"&lt;/span&gt;
    &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invalid&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;or&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;password"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;assertNotVisible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;My&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Tasks"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;clearState: true&lt;/code&gt; wipes the app's data before launching, so every test starts logged out. This is the single most important habit for a reliable suite: never let one test depend on what a previous test left behind.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;runFlow&lt;/code&gt; with &lt;code&gt;env&lt;/code&gt; overrides the variables for that sub-flow only.&lt;/li&gt;
&lt;li&gt;The selector &lt;code&gt;{ id: "loginError", text: "..." }&lt;/code&gt; matches an element that has that identifier &lt;strong&gt;and&lt;/strong&gt; that text.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Test 2: &lt;code&gt;02_login_success.yaml&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;com.example.TodoDemo&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Login succeeds with valid credentials&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;smoke&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;auth&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;launchApp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;clearState&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;notifications&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;allow&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;runFlow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;../subflows/login.yaml&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;runFlow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;../subflows/dismiss_permission_dialog.yaml&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;assertVisible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;My&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Tasks"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;assertVisible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;tasks&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;yet"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;takeScreenshot&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;after_login&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;permissions: { notifications: allow }&lt;/code&gt; grants the permission before the app starts, so the dialog should never appear. &lt;code&gt;takeScreenshot&lt;/code&gt; saves a PNG, which is useful for reviewing UI in CI artifacts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test 3: &lt;code&gt;03_add_and_complete_tasks.yaml&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;com.example.TodoDemo&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Add tasks and mark one as done&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;smoke&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;tasks&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;launchApp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;clearState&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;notifications&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;allow&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;runFlow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;../subflows/login.yaml&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;runFlow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;../subflows/dismiss_permission_dialog.yaml&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;newTaskField"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;inputText&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Buy&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;milk"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;addTaskButton"&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;newTaskField"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;inputText&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Write&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Maestro&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;article"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;addTaskButton"&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;assertVisible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Buy&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;milk"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;assertVisible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Write&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Maestro&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;article"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;assertVisible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;progressLabel"&lt;/span&gt;
    &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;done"&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Buy&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;milk"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;assertVisible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;progressLabel"&lt;/span&gt;
    &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;done"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice how the test reads almost exactly like a manual QA script. That readability is the main reason teams adopt Maestro.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test 4: &lt;code&gt;04_delete_task.yaml&lt;/code&gt; (loops, JavaScript and swipes)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;com.example.TodoDemo&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Delete a task with swipe&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;tasks&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;regression&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;launchApp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;clearState&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;notifications&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;allow&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;runFlow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;../subflows/login.yaml&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;runFlow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;../subflows/dismiss_permission_dialog.yaml&lt;/span&gt;

&lt;span class="c1"&gt;# Create "Task 1", "Task 2" and "Task 3" using a loop and a JS counter&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;evalScript&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${output.counter = 0}&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repeat&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;times&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
    &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;evalScript&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${output.counter = output.counter + 1}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;newTaskField"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;inputText&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Task&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${output.counter}"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;addTaskButton"&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;assertVisible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Task&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;3"&lt;/span&gt;

&lt;span class="c1"&gt;# Swipe left on the row to reveal the system Delete action&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;swipe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Task&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;2"&lt;/span&gt;
    &lt;span class="na"&gt;direction&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;LEFT&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Delete"&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;assertNotVisible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Task&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;2"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;assertVisible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Task&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;assertVisible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Task&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;3"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;assertVisible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;progressLabel"&lt;/span&gt;
    &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;done"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;output&lt;/code&gt; is a JavaScript object Maestro provides for storing values across steps.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;repeat&lt;/code&gt; runs a block a fixed number of times (it also supports a &lt;code&gt;while&lt;/code&gt; condition).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;swipe&lt;/code&gt; with &lt;code&gt;from&lt;/code&gt; starts the gesture on a specific element, which is how you trigger SwiftUI's &lt;code&gt;.onDelete&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use loops and scripts sparingly. If a flow starts to look like a program, consider seeding data through a test backend or launch arguments instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test 5: &lt;code&gt;05_session_and_logout.yaml&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;com.example.TodoDemo&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Session persists across restarts until logout&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;auth&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;regression&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;launchApp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;clearState&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;notifications&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;allow&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;runFlow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;../subflows/login.yaml&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;runFlow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;../subflows/dismiss_permission_dialog.yaml&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;assertVisible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;My&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Tasks"&lt;/span&gt;

&lt;span class="c1"&gt;# Restart without clearing state: the user should still be logged in&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;stopApp&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;launchApp&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;assertVisible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;My&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Tasks"&lt;/span&gt;

&lt;span class="c1"&gt;# Log out and confirm we are back on the login screen&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;logoutButton"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;assertVisible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Welcome"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;assertVisible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;loginButton"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This test checks behavior that crosses an app restart, something that is awkward to do in many in-process frameworks.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part D: Running the suite
&lt;/h2&gt;

&lt;p&gt;Make sure the Simulator is booted and the app is installed, then from the project root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run the whole suite&lt;/span&gt;
maestro &lt;span class="nb"&gt;test&lt;/span&gt; .maestro/

&lt;span class="c"&gt;# Run a single flow&lt;/span&gt;
maestro &lt;span class="nb"&gt;test&lt;/span&gt; .maestro/flows/02_login_success.yaml

&lt;span class="c"&gt;# Run only smoke tests&lt;/span&gt;
maestro &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--include-tags&lt;/span&gt; smoke .maestro/

&lt;span class="c"&gt;# Exclude slow tests&lt;/span&gt;
maestro &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--exclude-tags&lt;/span&gt; regression .maestro/

&lt;span class="c"&gt;# Pass variables from the command line&lt;/span&gt;
maestro &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;EMAIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;qa@example.com &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;secret .maestro/flows/02_login_success.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Faster authoring with continuous mode
&lt;/h3&gt;

&lt;p&gt;While writing a flow, use continuous mode. Maestro reruns the flow every time you save the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;maestro &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; .maestro/flows/03_add_and_complete_tasks.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Combined with Maestro Studio for finding selectors, this gives you a feedback loop of a few seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Validate syntax without a device
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;maestro check-syntax .maestro/flows/04_delete_task.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Useful as a pre-commit hook or a quick CI step.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reports and artifacts
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# JUnit XML, understood by most CI systems&lt;/span&gt;
maestro &lt;span class="nb"&gt;test&lt;/span&gt; .maestro/ &lt;span class="nt"&gt;--format&lt;/span&gt; junit &lt;span class="nt"&gt;--output&lt;/span&gt; build/maestro-report.xml

&lt;span class="c"&gt;# HTML report for humans&lt;/span&gt;
maestro &lt;span class="nb"&gt;test&lt;/span&gt; .maestro/ &lt;span class="nt"&gt;--format&lt;/span&gt; html &lt;span class="nt"&gt;--output&lt;/span&gt; build/maestro-report.html

&lt;span class="c"&gt;# Put screenshots, logs and per-flow artifacts in a known folder&lt;/span&gt;
maestro &lt;span class="nb"&gt;test&lt;/span&gt; .maestro/ &lt;span class="nt"&gt;--test-output-dir&lt;/span&gt; build/maestro-artifacts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a flow fails, the artifacts include a screenshot before each step, the view hierarchy at the moment of failure, device logs and crash reports. Start debugging there, not in the terminal output.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running on several simulators in parallel
&lt;/h3&gt;

&lt;p&gt;If you have more than one simulator booted, you can split the suite across them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;maestro &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--shard-split&lt;/span&gt; 2 .maestro/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--shard-split&lt;/code&gt; divides the flows between devices. &lt;code&gt;--shard-all&lt;/code&gt; runs every flow on every device, which is useful for testing several iOS versions or screen sizes at once.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part E: The same test in XCUITest
&lt;/h2&gt;

&lt;p&gt;To make the comparison from Part 1 concrete, here is roughly what Test 2 (successful login) looks like in XCUITest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;XCTest&lt;/span&gt;

&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;LoginUITests&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;XCTestCase&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;setUp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;continueAfterFailure&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;testLoginSucceeds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;XCUIApplication&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;launchArguments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"-isLoggedIn"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"NO"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;// reset @AppStorage&lt;/span&gt;
        &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;textFields&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"emailField"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="kt"&gt;XCTAssertTrue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForExistence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;typeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"demo@example.com"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;secureTextFields&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"passwordField"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;typeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"password123"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buttons&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"loginButton"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="c1"&gt;// System alerts live in SpringBoard, outside the app under test&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;springboard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;XCUIApplication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;bundleIdentifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"com.apple.springboard"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;allow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;springboard&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buttons&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;allow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForExistence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;allow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="kt"&gt;XCTAssertTrue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;navigationBars&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"My Tasks"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForExistence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="kt"&gt;XCTAssertTrue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;staticTexts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"No tasks yet"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;XCUITest is not bad. It is fast, it is Apple's own tooling and it runs on physical devices. But compare the two:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Maestro&lt;/th&gt;
&lt;th&gt;XCUITest&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lines for this test&lt;/td&gt;
&lt;td&gt;About 10 (plus a reusable sub-flow)&lt;/td&gt;
&lt;td&gt;About 30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Explicit waits&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;waitForExistence&lt;/code&gt; on most steps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;System dialogs&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tapOn: "Allow"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Separate SpringBoard app handle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resetting state&lt;/td&gt;
&lt;td&gt;&lt;code&gt;clearState: true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Launch arguments or custom reset code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rebuild after change&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes, test target recompiles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reusable on Android&lt;/td&gt;
&lt;td&gt;Often, with the same flow&lt;/td&gt;
&lt;td&gt;No, rewrite in Espresso&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Physical iPhones&lt;/td&gt;
&lt;td&gt;Not officially, see Part 1&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Many teams use both: XCUITest or unit tests for detailed component behavior, Maestro for end-to-end user journeys.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part F: Writing flows with an AI coding agent
&lt;/h2&gt;

&lt;p&gt;Maestro ships an MCP server inside the CLI. It lets an AI coding agent see the simulator, inspect the view hierarchy, run flows and read the results. To connect it, add this to your agent's MCP configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"maestro"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"maestro"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most agents also have a one-line command. For example, with the Codex CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;codex mcp add maestro &lt;span class="nt"&gt;--&lt;/span&gt; maestro mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and with the Gemini CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gemini mcp add maestro maestro mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you can ask something like: "Write a Maestro flow that logs in to TodoDemo, adds three tasks, completes the second one and checks the progress label. Run it and fix it until it passes."&lt;/p&gt;




&lt;h2&gt;
  
  
  Troubleshooting checklist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;What to check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Element not found&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Run &lt;code&gt;maestro hierarchy&lt;/code&gt; or open Studio. Is the identifier set on the element you think it is? Is the text an exact match?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tap lands on the keyboard&lt;/td&gt;
&lt;td&gt;The keyboard may cover the target. Add &lt;code&gt;- hideKeyboard&lt;/code&gt; or scroll first with &lt;code&gt;scrollUntilVisible&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test passes locally, fails in CI&lt;/td&gt;
&lt;td&gt;CI machines are slower. Use &lt;code&gt;extendedWaitUntil&lt;/code&gt; with a longer timeout for slow screens, and increase &lt;code&gt;MAESTRO_DRIVER_STARTUP_TIMEOUT&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests affect each other&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;launchApp: { clearState: true }&lt;/code&gt; at the start of every flow.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Long animations break assertions&lt;/td&gt;
&lt;td&gt;Add &lt;code&gt;- waitForAnimationToEnd&lt;/code&gt; before the assertion.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;.ipa&lt;/code&gt; will not install&lt;/td&gt;
&lt;td&gt;Maestro needs a Simulator build (&lt;code&gt;.app&lt;/code&gt;) built with &lt;code&gt;-sdk iphonesimulator&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flags stopped working after an upgrade&lt;/td&gt;
&lt;td&gt;Check the changelog. &lt;code&gt;--ios-version&lt;/code&gt; is being replaced by &lt;code&gt;--device-os&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Example of a longer explicit wait for a slow screen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;extendedWaitUntil&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;visible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;progressLabel"&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;15000&lt;/span&gt;   &lt;span class="c1"&gt;# milliseconds&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Best practices summary
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One user journey per flow.&lt;/strong&gt; Short flows fail for one clear reason.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reset state at the start of every flow&lt;/strong&gt; with &lt;code&gt;clearState: true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use identifiers for controls, text for content.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extract repeated steps into sub-flows&lt;/strong&gt; (login, onboarding, navigation).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep logic out of YAML&lt;/strong&gt; where possible. Seed data through APIs or launch arguments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tag your flows&lt;/strong&gt; (&lt;code&gt;smoke&lt;/code&gt;, &lt;code&gt;regression&lt;/code&gt;, feature names) and run smoke tests on every PR, the full suite nightly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pin the Maestro version&lt;/strong&gt; in CI and upgrade on purpose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always keep artifacts&lt;/strong&gt; from failed runs.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Ending
&lt;/h2&gt;

&lt;p&gt;With about 120 lines of Swift and five short YAML files, we have a test suite that covers login, error handling, adding, completing and deleting tasks, session persistence, logout and system permission dialogs. It runs locally in under a couple of minutes, runs in GitHub Actions without changes, and can move to a device cloud with one command.&lt;/p&gt;

&lt;p&gt;That is Maestro's real value: the distance between "we should test this" and "it is tested in CI" is very short.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/mobile-dev-inc/maestro" rel="noopener noreferrer"&gt;mobile-dev-inc/maestro&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ios</category>
      <category>testing</category>
      <category>mobile</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Maestro: Mobile UI Testing Without the Pain - Introduction</title>
      <dc:creator>ArshTechPro</dc:creator>
      <pubDate>Tue, 27 Oct 2026 19:10:57 +0000</pubDate>
      <link>https://dev.to/arshtechpro/maestro-mobile-ui-testing-without-the-pain-introduction-1fg8</link>
      <guid>https://dev.to/arshtechpro/maestro-mobile-ui-testing-without-the-pain-introduction-1fg8</guid>
      <description>&lt;p&gt;If you have ever written mobile end-to-end tests, you probably know the routine. You set up Appium, fight with drivers and capabilities, write a test that passes locally, and then watch it fail in CI because an animation took 200ms longer than expected. You add a &lt;code&gt;sleep(2)&lt;/code&gt;. Then another. Six months later nobody trusts the suite.&lt;/p&gt;

&lt;p&gt;Maestro is an open-source framework built to make that experience less painful. This article covers what it is, how it works, what you should know before adopting it, and how it stacks up against the other free and paid options.&lt;/p&gt;

&lt;p&gt;This is Part 1 of a two-part series. &lt;a href="https://dev.to/arshtechpro/maestro-deep-dive-testing-a-swiftui-app-end-to-end-with-ci-3bkp"&gt;Part 2&lt;/a&gt; is a hands-on deep dive with a small SwiftUI app and a complete test suite.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Maestro?
&lt;/h2&gt;

&lt;p&gt;Maestro is a UI and end-to-end testing framework for Android, iOS and web apps. Instead of writing tests in Java, Kotlin, Swift or JavaScript, you describe user journeys (called &lt;strong&gt;Flows&lt;/strong&gt;) in YAML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;com.example.myapp&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;launchApp&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sign&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Email"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;inputText&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jane@example.com"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Password"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;inputText&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hunter2"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Continue"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;assertVisible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Welcome,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Jane"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a complete, runnable test. No page objects, no driver setup code, no explicit waits.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Maestro works (and why it matters)
&lt;/h2&gt;

&lt;p&gt;Maestro is a &lt;strong&gt;black-box&lt;/strong&gt; tool. It does not link into your app, does not need an SDK, and does not need your source code. It drives the device the way a user would, and it reads the screen through the platform's accessibility layer and view hierarchy.&lt;/p&gt;

&lt;p&gt;On iOS, Maestro installs a small XCTest-based driver on the simulator. On Android, it installs a driver app that talks to UIAutomator. The CLI sends commands to the driver, reads back the UI hierarchy, and decides what to do next.&lt;/p&gt;

&lt;p&gt;Three design choices make it feel different from older tools:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Automatic waiting.&lt;/strong&gt; After each command, Maestro waits for the screen to settle before moving on. If you &lt;code&gt;tapOn: "Continue"&lt;/code&gt; and the button is not visible yet, it keeps looking for a while before failing. In practice this removes most of the &lt;code&gt;sleep()&lt;/code&gt; calls you would write elsewhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Flakiness tolerance.&lt;/strong&gt; Mobile UIs are noisy: taps sometimes do not register, elements shift during animations, keyboards appear and disappear. Maestro's commands are designed to retry and re-check rather than fail on the first miss.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Interpreted, not compiled.&lt;/strong&gt; Flows are read at runtime. Change a YAML file, rerun, and see the result in seconds. There is no test target to rebuild.&lt;/p&gt;

&lt;p&gt;Because it operates "at arm's length", Maestro can also do things in-process tools cannot: tap system permission dialogs, open Safari or Settings, and come back to your app in the same flow.&lt;/p&gt;




&lt;h2&gt;
  
  
  The ecosystem: CLI, Studio, MCP and Cloud
&lt;/h2&gt;

&lt;p&gt;Maestro is more than one binary. Here is what each piece does and what it costs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Open source?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maestro CLI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Runs flows locally and in CI. The core of everything.&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Yes (Apache 2.0)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maestro Studio (desktop)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Visual IDE: inspect elements, record interactions, build flows without the terminal. Available for macOS, Windows and Linux.&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maestro MCP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Built into the CLI (&lt;code&gt;maestro mcp&lt;/code&gt;). Lets AI coding agents such as Claude Code, Cursor, Copilot, Codex and Gemini run flows, inspect the screen and write tests.&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maestro Cloud&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hosted parallel execution on managed simulators, emulators and browsers.&lt;/td&gt;
&lt;td&gt;Paid&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A note on Studio: older versions of the CLI shipped a browser-based Studio (&lt;code&gt;maestro studio&lt;/code&gt;). That was removed in CLI 2.6.0 in favor of the desktop app. If you find older tutorials using &lt;code&gt;maestro studio&lt;/code&gt;, that is why they no longer match.&lt;/p&gt;

&lt;p&gt;A note on AI: the old &lt;code&gt;maestro chat&lt;/code&gt; command was discontinued in 2.7.0. The supported path for AI-assisted test authoring is now Maestro MCP.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting started in five minutes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Install
&lt;/h3&gt;

&lt;p&gt;You need Java 17 or newer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;java &lt;span class="nt"&gt;-version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then install the CLI on macOS, Linux or Windows (WSL):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; &lt;span class="s2"&gt;"https://get.maestro.mobile.dev"&lt;/span&gt; | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or, on macOS with Homebrew:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew tap mobile-dev-inc/tap
brew &lt;span class="nb"&gt;install &lt;/span&gt;maestro
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;maestro &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Start a device
&lt;/h3&gt;

&lt;p&gt;For iOS you need Xcode with at least one Simulator runtime installed. For Android, an emulator from Android Studio. Maestro can also start one for you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;maestro list-devices
maestro start-device &lt;span class="nt"&gt;--platform&lt;/span&gt; ios
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Write a flow
&lt;/h3&gt;

&lt;p&gt;Save this as &lt;code&gt;contacts.yaml&lt;/code&gt;. It uses the built-in iOS Contacts app, so you do not need your own app yet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;com.apple.MobileAddressBook&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;launchApp&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Add"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;First&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;name"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;inputText&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ada"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Last&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;name"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;inputText&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Lovelace"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Done"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;assertVisible&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ada&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Lovelace"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Button and field labels in Apple's built-in apps can change between iOS versions. If a step fails, open Maestro Studio or run &lt;code&gt;maestro hierarchy&lt;/code&gt; to see the exact labels on your simulator.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Run it
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;maestro &lt;span class="nb"&gt;test &lt;/span&gt;contacts.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will see each step printed as it runs, with a pass or fail marker. On failure, Maestro saves a screenshot, the view hierarchy and logs so you can see what was on screen at the moment it broke.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is important for developers to know
&lt;/h2&gt;

&lt;p&gt;These are the things that are easy to miss in the marketing material but matter once you adopt Maestro on a real project.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. iOS testing targets Simulators
&lt;/h3&gt;

&lt;p&gt;Maestro's officially supported local iOS target is the &lt;strong&gt;Simulator&lt;/strong&gt;. You need a Simulator build (&lt;code&gt;.app&lt;/code&gt;), not an App Store or Ad Hoc &lt;code&gt;.ipa&lt;/code&gt;. Support for physical iPhones has been one of the most requested features in the repository for a long time. Today, if you need real iOS hardware, your options are third-party projects or device clouds that have added Maestro support on physical devices (TestingBot is one example). Check the current docs before you plan around real devices.&lt;/p&gt;

&lt;p&gt;Android has no such limitation: emulators and USB-connected physical devices both work.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Accessibility identifiers make tests stable
&lt;/h3&gt;

&lt;p&gt;Maestro can find elements by visible text, which is great for getting started. For a test suite you plan to keep, add stable identifiers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SwiftUI / UIKit:&lt;/strong&gt; &lt;code&gt;accessibilityIdentifier&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Android:&lt;/strong&gt; &lt;code&gt;resource-id&lt;/code&gt; / &lt;code&gt;testTag&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React Native:&lt;/strong&gt; &lt;code&gt;testID&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flutter:&lt;/strong&gt; &lt;code&gt;Semantics(identifier: ...)&lt;/code&gt; or keys exposed through semantics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then target them with &lt;code&gt;id&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;tapOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;loginButton"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Text selectors break when copy changes or when you test in another language. IDs do not.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. YAML is simple until it isn't
&lt;/h3&gt;

&lt;p&gt;For linear user journeys, YAML is a joy. For complex logic (loops with conditions, computed data, calling a backend to seed state), you will reach for Maestro's JavaScript support (&lt;code&gt;runScript&lt;/code&gt;, &lt;code&gt;evalScript&lt;/code&gt;, &lt;code&gt;${...}&lt;/code&gt; expressions and an &lt;code&gt;http&lt;/code&gt; client). It works, but it is not a full programming language with an IDE, types and a debugger. If your test strategy depends on heavy logic, factor that into your choice.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Black-box means no app internals
&lt;/h3&gt;

&lt;p&gt;You cannot call into your app's code, stub a network layer from inside the process, or read internal state. If you need that, it has to happen outside Maestro: a test backend, launch arguments, or a debug-only screen. Maestro's own built-in network mocking was deprecated years ago.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. It is not a replacement for unit or in-process UI tests
&lt;/h3&gt;

&lt;p&gt;Maestro is excellent for high-level user journeys: sign-up, checkout, onboarding, critical paths. It is not the right tool for testing every state of a single component. Keep XCTest/XCUITest, Espresso or your framework's widget tests for that.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. CI is straightforward, scaling is the hard part
&lt;/h3&gt;

&lt;p&gt;Running Maestro in CI is just &lt;code&gt;maestro test&lt;/code&gt; plus JUnit or HTML reports. Scaling is where costs appear. iOS Simulators need macOS machines. Locally you can shard across several simulators (parallel iOS simulator execution became reliable in CLI 2.6.0), but at some point teams either buy more Mac runners or move to a cloud.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. The project moves fast
&lt;/h3&gt;

&lt;p&gt;The CLI ships frequently, and some releases deprecate flags. For example, &lt;code&gt;--ios-version&lt;/code&gt; and &lt;code&gt;--android-api-level&lt;/code&gt; are being replaced by &lt;code&gt;--device-os&lt;/code&gt; and &lt;code&gt;--device-model&lt;/code&gt;. Pin your Maestro version in CI and read the changelog when upgrading.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Maestro compares to other tools
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Free and open-source frameworks
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Platforms&lt;/th&gt;
&lt;th&gt;Test language&lt;/th&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Setup effort&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maestro&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;iOS, Android, Web&lt;/td&gt;
&lt;td&gt;YAML (+ JS)&lt;/td&gt;
&lt;td&gt;Black-box&lt;/td&gt;
&lt;td&gt;Minutes&lt;/td&gt;
&lt;td&gt;Cross-platform user journeys, mixed teams, fast feedback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Appium&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;iOS, Android, Web, Windows, more&lt;/td&gt;
&lt;td&gt;Java, JS/TS, Python, C#, Ruby&lt;/td&gt;
&lt;td&gt;Black-box (WebDriver)&lt;/td&gt;
&lt;td&gt;Hours to days&lt;/td&gt;
&lt;td&gt;Large cross-platform suites, teams with existing Selenium skills&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;XCUITest&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;iOS only&lt;/td&gt;
&lt;td&gt;Swift / Obj-C&lt;/td&gt;
&lt;td&gt;In Xcode, separate test process&lt;/td&gt;
&lt;td&gt;Low (built into Xcode)&lt;/td&gt;
&lt;td&gt;iOS-only teams who want native tooling and real-device support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Espresso&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Android only&lt;/td&gt;
&lt;td&gt;Kotlin / Java&lt;/td&gt;
&lt;td&gt;Gray-box, in-process&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Android-only teams, fast and stable component-level UI tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Detox&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;iOS, Android (React Native)&lt;/td&gt;
&lt;td&gt;JS / TS&lt;/td&gt;
&lt;td&gt;Gray-box, syncs with RN internals&lt;/td&gt;
&lt;td&gt;Moderate to high&lt;/td&gt;
&lt;td&gt;React Native apps that need tight synchronization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Flutter integration_test / Patrol&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;iOS, Android (Flutter)&lt;/td&gt;
&lt;td&gt;Dart&lt;/td&gt;
&lt;td&gt;In-process, Patrol adds native interaction&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;td&gt;Flutter teams who want tests in Dart&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Maestro vs Appium.&lt;/strong&gt; Appium is the most flexible and the most mature: any language, any platform, huge ecosystem, real iOS devices. It is also the most work to set up and maintain, and tests tend to need explicit waits. Maestro trades flexibility for speed of authoring and lower flakiness. Many teams that move from Appium to Maestro do so because test maintenance, not test execution, was their bottleneck.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maestro vs XCUITest.&lt;/strong&gt; XCUITest is Apple's own framework. It is fast, deeply integrated with Xcode and supports real devices. But tests are Swift code that needs a test target, compilation and more boilerplate, and it only covers iOS. If you ship on both platforms, you would also need Espresso and two separate suites. Maestro gives you one suite, often with shared flows across platforms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maestro vs Detox.&lt;/strong&gt; Detox is gray-box: it knows when React Native is idle, which makes it very stable for RN apps. That same coupling makes setup harder and limits it to React Native. Maestro works with any framework but synchronizes based on what is on screen rather than JS thread state.&lt;/p&gt;

&lt;h3&gt;
  
  
  Paid platforms and device clouds
&lt;/h3&gt;

&lt;p&gt;Most paid offerings are &lt;strong&gt;infrastructure&lt;/strong&gt;: they run tests you have already written on hosted devices. Prices below are list prices published at the time of writing (October 2026). Always confirm on the vendor's pricing page.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;What you get&lt;/th&gt;
&lt;th&gt;Pricing model&lt;/th&gt;
&lt;th&gt;Maestro support&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maestro Cloud&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Official hosted runner for Maestro flows, parallel execution, artifacts, notifications&lt;/td&gt;
&lt;td&gt;$250 per device per month for iOS or Android, $125 per browser per month for web; enterprise plans available; 7-day free trial&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;BrowserStack App Automate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Large real-device cloud for Appium, Espresso, XCUITest and more&lt;/td&gt;
&lt;td&gt;Per parallel session; App Automate starts around $199 per parallel per month billed annually&lt;/td&gt;
&lt;td&gt;Yes, via integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sauce Labs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Real devices and emulators, enterprise features&lt;/td&gt;
&lt;td&gt;Per parallel test with unlimited users and minutes&lt;/td&gt;
&lt;td&gt;Check current docs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TestingBot&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Real devices and simulators, Maestro CLI integration&lt;/td&gt;
&lt;td&gt;Subscription&lt;/td&gt;
&lt;td&gt;Yes, including physical iOS devices&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DeviceCloud (devicecloud.dev)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Maestro-focused cloud runner&lt;/td&gt;
&lt;td&gt;Pro plan from $25 per month including test credits, usage on top&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AWS Device Farm / Firebase Test Lab&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pay-as-you-go device access from the major clouds&lt;/td&gt;
&lt;td&gt;Metered usage with free tiers or trials&lt;/td&gt;
&lt;td&gt;Possible with custom setup, not first-class&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;AI-native testing tools&lt;/strong&gt; (for example Drizz, Autosana)&lt;/td&gt;
&lt;td&gt;Natural-language or vision-based test authoring and self-healing&lt;/td&gt;
&lt;td&gt;Subscription, usually quote-based&lt;/td&gt;
&lt;td&gt;Not applicable; they are alternatives&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A simple way to think about it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authoring cost&lt;/strong&gt; (how long tests take to write and maintain): Maestro is among the lowest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execution cost&lt;/strong&gt; (where tests run): free on your own machines, then you pay either for Mac CI runners or for a device cloud. The cloud choice is mostly about price per parallel, device coverage (real vs virtual) and where your data is allowed to go.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  When to choose Maestro
&lt;/h2&gt;

&lt;p&gt;Maestro is a strong fit if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You ship on both iOS and Android, or plan to.&lt;/li&gt;
&lt;li&gt;You want end-to-end coverage of critical user journeys without building a test framework.&lt;/li&gt;
&lt;li&gt;QA engineers, product people or designers should be able to read and contribute to tests.&lt;/li&gt;
&lt;li&gt;Flaky tests are a bigger problem for you than raw test speed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Look elsewhere, or combine tools, if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You must run iOS tests on physical devices in your own lab with official support.&lt;/li&gt;
&lt;li&gt;Your tests need deep access to app internals or in-process mocking.&lt;/li&gt;
&lt;li&gt;You need thousands of fine-grained UI assertions per component (use XCUITest, Espresso or framework tests for those).&lt;/li&gt;
&lt;li&gt;Your organization already has a large, healthy Appium suite.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Maestro's pitch is simple: describe what a user does, in plain YAML, and let the framework handle the waiting and retrying. For most teams the biggest win is not execution speed but the fact that tests are quick to write, easy to read and less likely to become the flaky suite nobody trusts.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;Part 2&lt;/strong&gt;, we will build a small SwiftUI app with a login screen and a to-do list, then write a full Maestro suite for it: stable selectors, sub-flows, environment variables, conditional logic for permission dialogs, tags, reports and a GitHub Actions workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/mobile-dev-inc/maestro" rel="noopener noreferrer"&gt;mobile-dev-inc/maestro&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ios</category>
      <category>mobile</category>
      <category>testing</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Xcode 27.2: What Developers Need to Know (Including the New JSON Project Format)</title>
      <dc:creator>ArshTechPro</dc:creator>
      <pubDate>Fri, 23 Oct 2026 11:15:00 +0000</pubDate>
      <link>https://dev.to/arshtechpro/xcode-272-what-developers-need-to-know-including-the-new-json-project-format-2bn2</link>
      <guid>https://dev.to/arshtechpro/xcode-272-what-developers-need-to-know-including-the-new-json-project-format-2bn2</guid>
      <description>&lt;p&gt;Xcode 27.2 is a point release, but it ships one change that will affect almost every team sooner or later: a new JSON-based project file that replaces the old &lt;code&gt;project.pbxproj&lt;/code&gt;. The rest of the release is a mix of small features, a solid batch of Device Hub and Previews fixes, and a few known issues you should know about before you upgrade your CI machines.&lt;/p&gt;

&lt;p&gt;This post walks through what matters, in order of how likely it is to affect your day.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: This is based on Apple's Xcode 27.2 release candidate notes. Details can still shift before the final release, so check the official notes linked at the end before making team-wide decisions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Requirements:&lt;/strong&gt; macOS Tahoe 26.6 or later. Ships with Swift 6.4 and the 27.2 SDKs for iOS, iPadOS, macOS, tvOS, watchOS, and visionOS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Biggest change:&lt;/strong&gt; a new JSON project format (&lt;code&gt;.xcproj&lt;/code&gt;) that is readable, diff-friendly, and much less prone to merge conflicts. It is the default for new projects and opt-in for existing ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compatibility gotcha:&lt;/strong&gt; &lt;code&gt;.xcproj&lt;/code&gt; projects open in Xcode 27 and later only. Anyone still on Xcode 26 cannot open them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coding Assistant:&lt;/strong&gt; a new &lt;code&gt;GetCodeCoverage&lt;/code&gt; MCP tool so agents can read coverage from your latest test run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Previews:&lt;/strong&gt; a new Display group in the canvas overrides picker for alternate displays, plus several reliability fixes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Device Hub:&lt;/strong&gt; many input-related fixes, including iOS 17 simulator input.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch out for:&lt;/strong&gt; a deployment-target bug in the non-iOS SDKs, Mac Catalyst compile errors with iOS 27.1-only APIs, and a few simulator quirks (workarounds below).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Requirements and platform support
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Xcode 27.2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Host macOS&lt;/td&gt;
&lt;td&gt;macOS Tahoe 26.6 or later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Swift&lt;/td&gt;
&lt;td&gt;6.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SDKs&lt;/td&gt;
&lt;td&gt;iOS, iPadOS, tvOS, watchOS, macOS, visionOS 27.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;On-device debugging&lt;/td&gt;
&lt;td&gt;iOS 17+, tvOS 17+, watchOS 10+, visionOS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If your build machines are still on an older macOS, update them first. That is usually the step that blocks CI upgrades, not Xcode itself.&lt;/p&gt;

&lt;p&gt;Apple also notes that the iOS SDK and simulator support for &lt;strong&gt;iPhone Duo&lt;/strong&gt; come from a separate download on the Apple Developer downloads page, so grab that if you are targeting the new hardware.&lt;/p&gt;

&lt;h2&gt;
  
  
  The headline: a JSON project format (&lt;code&gt;.xcproj&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;If you have ever resolved a merge conflict in &lt;code&gt;project.pbxproj&lt;/code&gt;, this is the section you care about.&lt;/p&gt;

&lt;h3&gt;
  
  
  What changed
&lt;/h3&gt;

&lt;p&gt;An &lt;code&gt;.xcodeproj&lt;/code&gt; bundle has always contained a project configuration file called &lt;code&gt;project.pbxproj&lt;/code&gt;. It is an old-style property list full of generated 24-character object IDs, cross-references, and comments. It works, but it is hard to read, hard to review, and famously painful to merge.&lt;/p&gt;

&lt;p&gt;Starting in Xcode 27.2, Xcode can store that configuration as a JSON file named &lt;code&gt;project.xcproj&lt;/code&gt; instead. According to Apple, the new file is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Smaller and hierarchical.&lt;/strong&gt; Structure is nested rather than a flat table of objects pointing at each other by ID.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-describing.&lt;/strong&gt; You can open it and understand what you are looking at.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easier to review.&lt;/strong&gt; Changes in a diff line up with what you actually did in Xcode, so a pull request that adds one file shows a small, readable change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Less conflict-prone.&lt;/strong&gt; Configuration changes are more isolated, so two people adding files to the same target are far less likely to collide.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent-friendly.&lt;/strong&gt; Coding assistants (Xcode's own or external ones over MCP) can edit it reliably instead of guessing at pbxproj IDs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The outer &lt;code&gt;.xcodeproj&lt;/code&gt; folder stays the same. Only the file inside it changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before:
MyApp.xcodeproj/
  project.pbxproj
  project.xcworkspace/
  xcshareddata/

After:
MyApp.xcodeproj/
  project.xcproj        &amp;lt;- new JSON file
  project.xcworkspace/
  xcshareddata/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So scripts and tools that just point at &lt;code&gt;MyApp.xcodeproj&lt;/code&gt; (for example &lt;code&gt;xcodebuild -project MyApp.xcodeproj&lt;/code&gt;) keep the same path. Tools that open and parse &lt;code&gt;project.pbxproj&lt;/code&gt; directly are a different story; more on that below.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is it the default?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;New projects&lt;/strong&gt; created in Xcode 27.2 use the JSON format by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Existing projects&lt;/strong&gt; keep using &lt;code&gt;project.pbxproj&lt;/code&gt; until you switch them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Xcode 27 and later understands both formats, so there is no forced migration.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to convert an existing project
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open the project in Xcode 27.2.&lt;/li&gt;
&lt;li&gt;Select the project in the &lt;strong&gt;Project navigator&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Open the &lt;strong&gt;File inspector&lt;/strong&gt; (right sidebar).&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;Project Document&lt;/strong&gt;, set &lt;strong&gt;Project Format&lt;/strong&gt; to &lt;strong&gt;JSON&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;If a confirmation dialog appears, click &lt;strong&gt;Continue&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Xcode removes &lt;code&gt;project.pbxproj&lt;/code&gt; and adds &lt;code&gt;project.xcproj&lt;/code&gt; inside your &lt;code&gt;.xcodeproj&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to undo it
&lt;/h3&gt;

&lt;p&gt;If you use source control, reverting is simple: discard the change that deleted &lt;code&gt;project.pbxproj&lt;/code&gt; and added &lt;code&gt;project.xcproj&lt;/code&gt;. For example, with Git:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git restore MyApp.xcodeproj/project.pbxproj
&lt;span class="nb"&gt;rm &lt;/span&gt;MyApp.xcodeproj/project.xcproj
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My recommendation: do the conversion as its own commit with nothing else in it. That makes it trivial to review and trivial to roll back.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compatibility: read this before converting a shared project
&lt;/h3&gt;

&lt;p&gt;This is the part that can bite a team.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.xcproj&lt;/code&gt; projects open in &lt;strong&gt;Xcode 27 and later&lt;/strong&gt;, including earlier Xcode 27.x releases.&lt;/li&gt;
&lt;li&gt;They do &lt;strong&gt;not&lt;/strong&gt; open in Xcode 26 or earlier.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before converting a shared repository, check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Every developer&lt;/strong&gt; on the team is on Xcode 27 or later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every CI runner&lt;/strong&gt; (GitHub Actions, Xcode Cloud, Bitrise, self-hosted, and so on) uses an Xcode 27 image.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Release branches&lt;/strong&gt; that still build with an older Xcode are not affected. Convert on your main branch, not on a maintenance branch that must keep building with Xcode 26.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Check your tooling
&lt;/h3&gt;

&lt;p&gt;A lot of iOS tooling reads or writes &lt;code&gt;project.pbxproj&lt;/code&gt; directly. Common examples include project generators, dependency managers that patch your project, release automation, and linters that inspect build settings or file membership. Until those tools announce support for &lt;code&gt;.xcproj&lt;/code&gt;, assume they may not understand it.&lt;/p&gt;

&lt;p&gt;Practical approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Convert on a branch.&lt;/li&gt;
&lt;li&gt;Run your full pipeline: dependency install, build, tests, signing, and any scripts that touch the project.&lt;/li&gt;
&lt;li&gt;Only merge once everything passes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you generate your project from a spec (for example with a project generator), you may not need to convert at all; keep generating whatever format your tool supports and revisit later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Apple open sourced the format
&lt;/h3&gt;

&lt;p&gt;Apple published the format as an open source Swift package, &lt;strong&gt;xcode-project-format&lt;/strong&gt; (Apache 2.0). It models the whole project (targets, groups, file references, build phases, build rules, and more) as typed Swift values under an &lt;code&gt;XCSchema&lt;/code&gt; namespace, and it includes a small CLI called &lt;code&gt;xcprojformatter&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That matters because tool authors no longer need to reverse-engineer the format. If you maintain internal scripts that poke at project files, this is the library to build on.&lt;/p&gt;

&lt;p&gt;Add it to a package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="nv"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;package&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"https://github.com/apple/xcode-project-format/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"0.1.0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A quick script that prints every target in a project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;Foundation&lt;/span&gt;
&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;XcodeProjectFormat&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;fileURLWithPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"MyApp.xcodeproj/project.xcproj"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="kt"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;contentsOf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;project&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="kt"&gt;XCSchema&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;Project&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;jsonRepresentation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;targets&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The package requires Swift 6.1 or later and macOS 14 or later, and also builds on non-Apple platforms, which is handy for Linux-based CI checks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should you switch now?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Switch now if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your whole team and CI are already on Xcode 27.&lt;/li&gt;
&lt;li&gt;You regularly fight &lt;code&gt;project.pbxproj&lt;/code&gt; merge conflicts.&lt;/li&gt;
&lt;li&gt;You use coding agents to add files, targets, or build settings.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Wait if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Anyone still needs to open the project in Xcode 26.&lt;/li&gt;
&lt;li&gt;You depend on tools that edit the project file and have not confirmed &lt;code&gt;.xcproj&lt;/code&gt; support.&lt;/li&gt;
&lt;li&gt;You generate the project from a spec and the generator does not output &lt;code&gt;.xcproj&lt;/code&gt; yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Coding Assistant: code coverage over MCP
&lt;/h2&gt;

&lt;p&gt;Xcode 27.2 adds a &lt;strong&gt;&lt;code&gt;GetCodeCoverage&lt;/code&gt;&lt;/strong&gt; MCP tool. It reports code coverage from your most recent test result, and you can filter it by target or by file.&lt;/p&gt;

&lt;p&gt;Why it is useful: an agent can now run your tests, check which lines are not covered, and write tests for exactly those gaps, all without you exporting coverage reports by hand. If you use an external agent connected to Xcode over MCP, it gets access to this tool too.&lt;/p&gt;

&lt;p&gt;Related fix: the &lt;strong&gt;&lt;code&gt;RenderPreview&lt;/code&gt;&lt;/strong&gt; MCP tool now returns the list of available render destinations and lets the caller choose which one to render on. Agents that render SwiftUI previews can now target a specific device instead of whatever the default happens to be.&lt;/p&gt;

&lt;h2&gt;
  
  
  Previews
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;New:&lt;/strong&gt; the canvas overrides picker now has a &lt;strong&gt;Display&lt;/strong&gt; group, so you can preview content on a device's alternate display. If you are building for hardware with more than one screen (such as iPhone Duo), this is how you check the second display without running the app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fixed:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Previews no longer fail in source files that use Windows-style CRLF line endings. If you share code with Windows-based tooling or have files that came through a cross-platform repo, this one may have been silently breaking your canvas.&lt;/li&gt;
&lt;li&gt;If another user account on the same Mac owns the Previews JIT directory, Xcode now shows a clear error with recovery steps instead of a vague failure. Shared build Macs and multi-user setups hit this.&lt;/li&gt;
&lt;li&gt;Clearer error when the selected platform cannot show a preview's content.&lt;/li&gt;
&lt;li&gt;Selecting a preview from a preview group's thumbnail grid on macOS now reliably opens the right one.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Device Hub and Simulator fixes
&lt;/h2&gt;

&lt;p&gt;Device Hub got a lot of input-handling fixes. If you mirror and control devices or simulators from Xcode, most of these will feel like things just started working:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keyboard and mouse input now works for simulators running older OS versions (before iOS 18, tvOS 18, watchOS 11, and visionOS 2).&lt;/li&gt;
&lt;li&gt;Device Hub now supports input to &lt;strong&gt;iOS 17 simulators&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Device Hub no longer stops sending touch and button input to a connected device after it has been idle for a while.&lt;/li&gt;
&lt;li&gt;Disconnecting a physical device while &lt;strong&gt;Simulate Hardware Keyboard&lt;/strong&gt; was on no longer leaves it stuck in hardware keyboard mode (which used to hide the software keyboard for up to two minutes).&lt;/li&gt;
&lt;li&gt;Modifier keys (Shift, Control, Option, Command) now go to the right device when multiple device windows are open.&lt;/li&gt;
&lt;li&gt;Holding Escape now exits keyboard capture mode.&lt;/li&gt;
&lt;li&gt;Deleted simulator runtimes are now fully removed, instead of reappearing after a reboot.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Known issues to plan around
&lt;/h2&gt;

&lt;p&gt;These are the ones most likely to cost you time.&lt;/p&gt;

&lt;h3&gt;
  
  
  SDK deployment target bug (macOS, watchOS, tvOS, visionOS)
&lt;/h3&gt;

&lt;p&gt;The macOS, watchOS, tvOS, and visionOS SDKs in Xcode 27.2 incorrectly list &lt;strong&gt;27.1&lt;/strong&gt; as a valid deployment target. If you pick it, builds and other features may behave unexpectedly. Mac Catalyst builds with a 27.1 or 27.2 deployment target may also be unable to use newly introduced APIs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to do:&lt;/strong&gt; do not set your deployment target to 27.1 on those platforms with this Xcode. Stick with the target you already had, or 27.2 if you need the new APIs and are not building for Catalyst.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mac Catalyst: compile errors with iOS 27.1-only APIs
&lt;/h3&gt;

&lt;p&gt;Projects that use APIs specific to iOS 27.1 can fail to compile for Mac Catalyst with errors like "undeclared identifier", "has no member", or "cannot find".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workaround:&lt;/strong&gt; wrap the affected code in a compile-time check.&lt;/p&gt;

&lt;p&gt;Swift:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="cp"&gt;#if !targetEnvironment(macCatalyst)&lt;/span&gt;
&lt;span class="c1"&gt;// iOS 27.1-only API usage here&lt;/span&gt;
&lt;span class="cp"&gt;#endif&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Objective-C:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight objective_c"&gt;&lt;code&gt;&lt;span class="cp"&gt;#if !TARGET_OS_MACCATALYST
&lt;/span&gt;&lt;span class="c1"&gt;// iOS 27.1-only API usage here&lt;/span&gt;
&lt;span class="cp"&gt;#endif
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Simulator runtime shows as unavailable after reinstalling
&lt;/h3&gt;

&lt;p&gt;If you delete a simulator runtime and reinstall it, Xcode may still show it as unavailable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workaround:&lt;/strong&gt; restart the CoreSimulator service.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;killall &lt;span class="nt"&gt;-9&lt;/span&gt; com.apple.CoreSimulator.CoreSimulatorService
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Device Hub crashes when selecting a Vision Pro simulator without a runtime
&lt;/h3&gt;

&lt;p&gt;If no visionOS runtime is installed, selecting a Vision Pro simulator in Device Hub can make Device Hub quit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workaround:&lt;/strong&gt; install the matching visionOS runtime from Xcode, or delete that simulator device.&lt;/p&gt;

&lt;h3&gt;
  
  
  Smaller ones
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Simulate Memory Warning&lt;/strong&gt; (Device menu) does not work. If you test memory-pressure handling, trigger it another way for now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;iPhone Duo screenshots and recordings&lt;/strong&gt; can come out black for up to a few minutes after the device boots. Wait a bit before capturing App Store screenshots.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloning a 27.2 simulator device&lt;/strong&gt; can fail due to incorrect file permissions. If you script simulator cloning for parallel tests, watch for this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accessibility tools&lt;/strong&gt; (VoiceOver, Accessibility Inspector) cannot read screen content on iPhone Duo inside Device Hub. Test accessibility on a physical device or a regular simulator window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CarPlay in Device Hub&lt;/strong&gt; needs a USB-connected device. Network-connected devices and the iOS Simulator are not supported for CarPlay.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Upgrade checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Update build Macs to macOS Tahoe 26.6 or later.&lt;/li&gt;
&lt;li&gt;Install Xcode 27.2 side by side with your current Xcode first, and run your full test suite.&lt;/li&gt;
&lt;li&gt;If you target iPhone Duo, download its SDK and simulator support separately.&lt;/li&gt;
&lt;li&gt;Check your non-iOS deployment targets are not set to 27.1.&lt;/li&gt;
&lt;li&gt;If you ship Mac Catalyst, look for iOS 27.1-only API usage and add &lt;code&gt;#if !targetEnvironment(macCatalyst)&lt;/code&gt; guards.&lt;/li&gt;
&lt;li&gt;Decide on &lt;code&gt;.xcproj&lt;/code&gt;: convert only when the whole team and CI are on Xcode 27, and do it as a standalone commit.&lt;/li&gt;
&lt;li&gt;If you use coding agents, try &lt;code&gt;GetCodeCoverage&lt;/code&gt; to have them target untested code.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Most of Xcode 27.2 is polish: better Device Hub input, sturdier Previews, and a couple of handy MCP tools for agents. The JSON project format is the real story. It fixes one of the oldest pain points in Apple development, and because it is open source, the tooling ecosystem can catch up quickly. Give your CI and tools a test run on a branch, and once everything is green, the days of untangling &lt;code&gt;project.pbxproj&lt;/code&gt; conflicts are over.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.apple.com/documentation/xcode-release-notes/xcode-27_2-release-notes" rel="noopener noreferrer"&gt;Xcode 27.2 Release Notes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ios</category>
      <category>mobile</category>
      <category>swift</category>
      <category>programming</category>
    </item>
    <item>
      <title>Supabase in 2026: Is This Open Source Backend Worth a Try?</title>
      <dc:creator>ArshTechPro</dc:creator>
      <pubDate>Tue, 20 Oct 2026 11:35:33 +0000</pubDate>
      <link>https://dev.to/arshtechpro/supabase-in-2026-is-this-open-source-backend-worth-a-try-ek8</link>
      <guid>https://dev.to/arshtechpro/supabase-in-2026-is-this-open-source-backend-worth-a-try-ek8</guid>
      <description>&lt;p&gt;If you have ever started a side project and spent the first weekend wiring up a database, user sign-ups, file uploads and an API before writing a single feature, Supabase was built for you.&lt;/p&gt;

&lt;p&gt;In this article we will look at what Supabase actually is, how it works under the hood, how to get a project running in a few minutes, and whether it is worth adding to your toolbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Supabase?
&lt;/h2&gt;

&lt;p&gt;Supabase describes itself as "the Postgres development platform." The short version: you get a real Postgres database, plus the backend pieces most apps need, without having to build them yourself.&lt;/p&gt;

&lt;p&gt;Out of the box you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A hosted Postgres database&lt;/strong&gt; (a full one, not a limited abstraction)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentication&lt;/strong&gt; with email, magic links, OAuth providers and more&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-generated APIs&lt;/strong&gt; (REST and GraphQL) based on your tables&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Realtime&lt;/strong&gt; subscriptions to database changes over websockets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage&lt;/strong&gt; for files like images and documents&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge Functions&lt;/strong&gt; for custom server-side logic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vector support&lt;/strong&gt; through pgvector for AI and embeddings work&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A dashboard&lt;/strong&gt; with a table editor, SQL editor and logs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is often called an open source Firebase alternative. The big difference is that Supabase is built on SQL and Postgres instead of a proprietary NoSQL store, so your data model is relational and portable.&lt;/p&gt;

&lt;p&gt;The project is very active: the main repository has over 100k stars on GitHub and is licensed under Apache 2.0.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works under the hood
&lt;/h2&gt;

&lt;p&gt;One thing I like about Supabase is that it is not a black box. It is a collection of open source tools, glued together:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Postgres&lt;/td&gt;
&lt;td&gt;The database at the center of everything&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgREST&lt;/td&gt;
&lt;td&gt;Turns your database schema into a REST API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GoTrue (Auth)&lt;/td&gt;
&lt;td&gt;JWT-based sign-up, login and sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Realtime&lt;/td&gt;
&lt;td&gt;An Elixir server that broadcasts database changes over websockets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage API&lt;/td&gt;
&lt;td&gt;S3-compatible file storage, with permissions handled in Postgres&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pg_graphql&lt;/td&gt;
&lt;td&gt;A Postgres extension that exposes a GraphQL API&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Because everything sits on top of Postgres, permissions are handled with Postgres Row Level Security (RLS). That means your security rules live next to your data, not scattered across your app code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started in 5 minutes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Create a project
&lt;/h3&gt;

&lt;p&gt;Sign up at &lt;a href="https://supabase.com" rel="noopener noreferrer"&gt;supabase.com&lt;/a&gt;, create a new project, and grab two values from the project settings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your &lt;strong&gt;project URL&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Your &lt;strong&gt;publishable (anon) key&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Create a table
&lt;/h3&gt;

&lt;p&gt;Open the SQL editor in the dashboard and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;todos&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;generated&lt;/span&gt; &lt;span class="n"&gt;always&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="k"&gt;identity&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;references&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;done&lt;/span&gt; &lt;span class="nb"&gt;boolean&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;todos&lt;/span&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="k"&gt;level&lt;/span&gt; &lt;span class="k"&gt;security&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"Users can manage their own todos"&lt;/span&gt;
&lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;todos&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;all&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="k"&gt;check&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last part matters. With RLS enabled and this policy in place, each user can only see and edit their own rows, even though the frontend talks to the database directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Install the client
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @supabase/supabase-js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@supabase/supabase-js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://your-project.supabase.co&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;your-publishable-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Sign up a user
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signUp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dev@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a-strong-password&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Read and write data
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Insert&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;todos&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Write a dev.to article&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Select&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;todos&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;todos&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;created_at&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ascending&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No API routes, no ORM setup, no controller files. The REST API already exists because the table exists.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Listen for changes in realtime
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;supabase&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;todos-changes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;postgres_changes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INSERT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;todos&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;New todo:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is enough to build a live-updating todo app, chat, or dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local development
&lt;/h2&gt;

&lt;p&gt;You do not have to develop against the cloud. The Supabase CLI runs the whole stack locally with Docker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx supabase init
npx supabase start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you a local database, auth, storage and dashboard. You can write migrations, test them locally, and push them to your hosted project when you are ready. It also generates TypeScript types from your schema, which makes the client fully typed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx supabase gen types typescript &lt;span class="nt"&gt;--local&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; database.types.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to avoid cloud hosting entirely, Supabase can also be self-hosted with Docker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Client libraries
&lt;/h2&gt;

&lt;p&gt;Official clients exist for JavaScript/TypeScript, Flutter, Swift and Python. The community maintains clients for C#, Kotlin, Go, Ruby and others. So whether you are building a Next.js app, a mobile app or a Python backend, you are covered.&lt;/p&gt;

&lt;h2&gt;
  
  
  The good and the not-so-good
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What works well
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It is just Postgres.&lt;/strong&gt; You can use SQL, joins, views, triggers, extensions and any Postgres tool you already know. If you ever leave, you take a standard Postgres database with you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Very fast to start.&lt;/strong&gt; Auth, storage and an API in minutes is not marketing speak; it really is that quick.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open source.&lt;/strong&gt; You can read the code, self-host it, and avoid hard lock-in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strong docs and community.&lt;/strong&gt; The docs are good, and the Discord and GitHub discussions are active.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Good fit for AI apps.&lt;/strong&gt; pgvector support means you can store embeddings next to your regular data.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What to watch out for
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You need to learn RLS.&lt;/strong&gt; If you forget to enable Row Level Security or write a loose policy, your data can be exposed. Take the time to understand it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logic in the database is a mindset shift.&lt;/strong&gt; Policies, functions and triggers in SQL can feel unfamiliar if you are used to keeping all logic in your backend code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free tier limits.&lt;/strong&gt; The free plan is generous for learning and prototypes, but inactive free projects can be paused, and real production apps will usually need a paid plan. Check the &lt;a href="https://supabase.com/pricing" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt; for current details.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosting takes effort.&lt;/strong&gt; It is possible, but running the full stack yourself in production means you own upgrades, backups and monitoring.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  So, is it worth a try?
&lt;/h2&gt;

&lt;p&gt;Yes, for most developers it is.&lt;/p&gt;

&lt;p&gt;Supabase is a great choice if you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Want to ship a side project, MVP or SaaS quickly&lt;/li&gt;
&lt;li&gt;Prefer SQL and relational data over document databases&lt;/li&gt;
&lt;li&gt;Are building frontend-heavy or mobile apps and do not want to write a custom backend&lt;/li&gt;
&lt;li&gt;Want an AI-ready database with vector search built in&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It may not be the best fit if you need a highly custom backend architecture, have strict requirements that rule out managed services and do not want to self-host, or your team has no interest in learning Postgres.&lt;/p&gt;

&lt;p&gt;The best way to decide is to build something small. Spin up a free project, create one table, turn on RLS, and connect a frontend. In an afternoon you will know whether Supabase fits the way you like to work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Useful links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/supabase/supabase" rel="noopener noreferrer"&gt;github.com/supabase/supabase&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>database</category>
      <category>postgres</category>
      <category>opensource</category>
      <category>programming</category>
    </item>
    <item>
      <title>A Free Prerequisite Graph of Everything Kids Learn: Exploring Marble's os-taxonomy</title>
      <dc:creator>ArshTechPro</dc:creator>
      <pubDate>Tue, 20 Oct 2026 09:09:20 +0000</pubDate>
      <link>https://dev.to/arshtechpro/a-free-prerequisite-graph-of-everything-kids-learn-exploring-marbles-os-taxonomy-1b52</link>
      <guid>https://dev.to/arshtechpro/a-free-prerequisite-graph-of-everything-kids-learn-exploring-marbles-os-taxonomy-1b52</guid>
      <description>&lt;p&gt;If you have ever tried to build anything in education tech, you have probably hit the same wall: curriculum data is either a flat list of standards in a PDF, or it is locked inside someone's product.&lt;/p&gt;

&lt;p&gt;A flat list tells you &lt;em&gt;what&lt;/em&gt; a child should learn in Year 2. It does not tell you &lt;em&gt;what they need to know first&lt;/em&gt;. And that second question is the one that matters if you are building adaptive practice, a tutoring app, a learning-path generator, or an AI tutor that needs to know where a kid is stuck.&lt;/p&gt;

&lt;p&gt;os-taxonomy from Marble is an attempt to fix that. It is an open dataset that breaks primary and elementary learning into small, teachable ideas and connects them with prerequisite edges. It has picked up around 4.8k stars, which is a lot for a pure data repo.&lt;/p&gt;

&lt;p&gt;Here is what is in it, how to use it, and whether it is worth your time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you actually get
&lt;/h2&gt;

&lt;p&gt;It is a graph, shipped as plain JSON.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;1,591 micro-topics (nodes).&lt;/strong&gt; Each is one teachable idea, like "Building sentences" or "Apparent brightness of stars".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3,225 prerequisite edges.&lt;/strong&gt; "Topic X depends on topic Y", each tagged &lt;code&gt;hard&lt;/code&gt; or &lt;code&gt;soft&lt;/code&gt;, with a one-line reason.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Curriculum alignment.&lt;/strong&gt; Topics link to the standards they came from: Common Core, NGSS, the UK National Curriculum, the C3 social studies framework, and IB PYP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;176 domain clusters.&lt;/strong&gt; Short, parent-friendly summaries per subject, domain, and age band.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Teaching research.&lt;/strong&gt; 1,051 records on &lt;em&gt;how&lt;/em&gt; to teach Science and Mathematics topics, with activities, feedback ideas, and assessment guidance. More on this below.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Coverage is weighted toward the core subjects:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Subject&lt;/th&gt;
&lt;th&gt;Topics&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Science&lt;/td&gt;
&lt;td&gt;547&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mathematics&lt;/td&gt;
&lt;td&gt;504&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;English&lt;/td&gt;
&lt;td&gt;286&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;History&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Personal &amp;amp; Social Development&lt;/td&gt;
&lt;td&gt;88&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Life Skills&lt;/td&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Computing&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Learning to Learn&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There is no runtime, no package, and no dependencies. You clone the repo and read JSON files.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data model
&lt;/h2&gt;

&lt;p&gt;Everything lives in &lt;code&gt;data/&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Contents&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;topics.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The micro-topics (nodes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dependencies.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Prerequisite edges&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;curriculum-standards.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Source standards, grouped by curriculum&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;clusters.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Domain summaries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;teaching-research.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Teaching guidance per topic (about 23 MB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;manifest.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Counts and SHA-256 checksums&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;JSON Schemas for each file are in &lt;code&gt;schema/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A topic looks like this (trimmed):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mt_N8CpN1EJrP"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CONCEPTUAL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"English"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Grammar &amp;amp; Punctuation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Building sentences"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ageRangeStart"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ageRangeEnd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"centrality"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.257&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"evidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"Distinguish between complete sentences and fragments"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"Compose a complete sentence with a subject and verb"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"assessmentPrompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"If {{name}} says something like &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;The dog&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;, can they tell you..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"standards"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ccss-ela:L.K.1f"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uk-nc-2013:Eng.App2.Y1.Sent.1"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few fields are worth calling out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;type&lt;/code&gt;&lt;/strong&gt; classifies the kind of knowledge: conceptual, procedural, representational, language, or meta.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;evidence&lt;/code&gt;&lt;/strong&gt; is a list of observable mastery criteria. This is the useful bit for anyone building assessments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;centrality&lt;/code&gt;&lt;/strong&gt; gives you a rough sense of how connected a topic is in the graph.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;assessmentPrompt&lt;/code&gt;&lt;/strong&gt; is a natural-language check with a &lt;code&gt;{{name}}&lt;/code&gt; placeholder for the child's name. Replace or strip it before showing it to anyone.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And an edge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"topicId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mt__00ZSLnB7p"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"prerequisiteId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mt_VBl1T1sFCM"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"strength"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hard"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Must understand vibrations make sound before finding volume patterns"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it as "&lt;code&gt;topicId&lt;/code&gt; depends on &lt;code&gt;prerequisiteId&lt;/code&gt;". Flip it and you get "unlocks".&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started in five minutes
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/withmarbleapp/os-taxonomy.git
&lt;span class="nb"&gt;cd &lt;/span&gt;os-taxonomy
node scripts/validate.mjs   &lt;span class="c"&gt;# checks structure, references, and checksums&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Find the direct prerequisites of a topic
&lt;/h3&gt;

&lt;p&gt;This uses JSON import attributes, so use a recent Node version (22 or newer is safest):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;topics&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./data/topics.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="kd"&gt;with&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;deps&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./data/dependencies.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="kd"&gt;with&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;byId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;topics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;topics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prereqsOf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;deps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dependencies&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;topicId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;byId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prerequisiteId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// "Addition and subtraction word problems"&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;prereqsOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mt_yJmvUCCym7&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;// [ 'Representing Addition and Subtraction' ]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Build a full learning path
&lt;/h3&gt;

&lt;p&gt;The interesting thing about a prerequisite graph is that you can walk it. Since the graph is a DAG (no cycles), a depth-first walk gives you every topic a learner needs before the target, in an order they could actually study them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;edgesByTopic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;deps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;edgesByTopic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;topicId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;edgesByTopic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;topicId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
  &lt;span class="nx"&gt;edgesByTopic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;topicId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;learningPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;hardOnly&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;edgesByTopic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hardOnly&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;strength&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nf"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prerequisiteId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;byId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// prerequisites first, target last&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nf"&gt;learningPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mt_yJmvUCCym7&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ageRangeStart&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ageRangeEnd&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;  &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;  &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running it on "Addition and subtraction word problems" prints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;4-6  Mathematics  One-to-one counting
4-6  Mathematics  How Many in Total?
4-6  Mathematics  Addition as combining or putting together two
4-6  Mathematics  Subtraction as taking away or separating
4-6  Mathematics  Representing Addition and Subtraction
4-6  Mathematics  Addition and subtraction word problems
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a sensible sequence: count, combine, take away, represent it, then solve word problems.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;hardOnly&lt;/code&gt; flag is handy. Hard edges are true blockers. Soft edges are "helpful to know", so include them when you want a gentler path and drop them when you want the shortest one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prefer Python?
&lt;/h3&gt;

&lt;p&gt;Same idea with &lt;code&gt;networkx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;networkx&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;nx&lt;/span&gt;

&lt;span class="n"&gt;topics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data/topics.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;topics&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
&lt;span class="n"&gt;deps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data/dependencies.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dependencies&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;G&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DiGraph&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;deps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;G&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prerequisiteId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;topicId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;strength&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;strength&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mt_yJmvUCCym7&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;needed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ancestors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;G&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;topological_sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;G&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subgraph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;needed&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tid&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;topics&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tid&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once it is in &lt;code&gt;networkx&lt;/code&gt;, you also get the usual graph tools for free: finding the most "foundational" topics, spotting bottlenecks, or checking which topics unlock the most downstream learning. Note that this version follows soft edges too, so it can return a longer path than the JavaScript one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add the teaching guidance
&lt;/h3&gt;

&lt;p&gt;The newest file, &lt;code&gt;teaching-research.json&lt;/code&gt;, covers what to &lt;em&gt;do&lt;/em&gt; with a topic. Each record joins to a topic by &lt;code&gt;topicId&lt;/code&gt; and includes a teaching summary, concrete activities with feedback ideas, common misconceptions, assessment guidance, and sources.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;research&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./data/teaching-research.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="kd"&gt;with&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;guidanceByTopic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;research&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;records&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;topicId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;guidance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;guidanceByTopic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mt_yJmvUCCym7&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;guidance&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;learn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;guidance&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;learn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;activities&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// - Story-modelling with counters and a mat&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only Science and Mathematics are covered so far. For English and the other subjects, &lt;code&gt;guidance&lt;/code&gt; will be &lt;code&gt;undefined&lt;/code&gt;, so keep that &lt;code&gt;?.&lt;/code&gt; in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you could build with it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Adaptive practice.&lt;/strong&gt; When a kid fails a topic, walk back one hard edge and test the prerequisite instead of repeating the same question.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI tutors.&lt;/strong&gt; Give an LLM the topic, its &lt;code&gt;evidence&lt;/code&gt; list, its prerequisites, and (for maths and science) its teaching activities as structured context. That grounds the model far better than "teach my kid fractions".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Placement and diagnostics.&lt;/strong&gt; Use &lt;code&gt;evidence&lt;/code&gt; and &lt;code&gt;assessmentPrompt&lt;/code&gt; to build quick checks, then use the graph to infer what a child probably already knows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Curriculum mapping.&lt;/strong&gt; Because topics link to both Common Core and the UK National Curriculum, you can roughly map content between the two systems.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visualizations.&lt;/strong&gt; The repo includes a 3D graph render, and Marble has an &lt;a href="https://withmarble.com/curriculum" rel="noopener noreferrer"&gt;interactive version&lt;/a&gt; where you can click a topic and see everything that leads to it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The caveats, honestly
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The licensing needs a careful read.&lt;/strong&gt; This is the most important part. The dataset has three layers with different terms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;database&lt;/strong&gt; (structure, IDs, relationships) is under ODbL 1.0.&lt;/li&gt;
&lt;li&gt;Marble's &lt;strong&gt;written text&lt;/strong&gt; (names, descriptions, evidence, prompts, reasons) is under CC BY-SA 4.0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;curriculum-standards.json&lt;/code&gt;&lt;/strong&gt; is not Marble's to license. Each source keeps its upstream terms.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  So, is it worth a try?
&lt;/h2&gt;

&lt;p&gt;If you work anywhere near education tech, yes. It takes minutes to load, and a well-structured prerequisite graph with mastery criteria is the kind of thing teams usually spend months building internally. Even if you do not use it as-is, it is a good reference model for how to structure learning data.&lt;/p&gt;

&lt;p&gt;It is especially useful as grounding data for AI tutoring tools, where the hard problem is often not generating explanations but knowing &lt;em&gt;what to teach next&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Just read the license section before shipping anything commercial, and treat v1 as a strong foundation that you validate, not a finished curriculum.&lt;/p&gt;

&lt;p&gt;A good first experiment: pick a topic your own kid (or a niece, nephew, or student) is struggling with, run &lt;code&gt;learningPath()&lt;/code&gt; on it, and see whether the prerequisites match what you see in real life.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Links&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repository: &lt;a href="https://github.com/withmarbleapp/os-taxonomy" rel="noopener noreferrer"&gt;github.com/withmarbleapp/os-taxonomy&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>opensource</category>
    </item>
    <item>
      <title>CLI-Anything: Turning Desktop Software into Tools Your AI Agent Can Actually Use</title>
      <dc:creator>ArshTechPro</dc:creator>
      <pubDate>Tue, 20 Oct 2026 04:52:40 +0000</pubDate>
      <link>https://dev.to/arshtechpro/cli-anything-turning-desktop-software-into-tools-your-ai-agent-can-actually-use-42fg</link>
      <guid>https://dev.to/arshtechpro/cli-anything-turning-desktop-software-into-tools-your-ai-agent-can-actually-use-42fg</guid>
      <description>&lt;p&gt;If you have used an AI coding agent like Claude Code, Codex, or Cursor for more than a week, you have probably noticed something: agents are great in a terminal and pretty bad everywhere else.&lt;/p&gt;

&lt;p&gt;Ask an agent to refactor a Python module and it does fine. Ask it to cut a video in Shotcut, add a layer in GIMP, or export a diagram from Draw.io, and things fall apart. The usual workaround is GUI automation: take a screenshot, guess where the button is, click, take another screenshot, repeat. It is slow, expensive in tokens, and breaks the moment a dialog moves.&lt;/p&gt;

&lt;p&gt;CLI-Anything from the Data Intelligence Lab at the University of Hong Kong (HKUDS), takes a different route. Instead of teaching agents to use GUIs, it gives the software a proper command-line interface that agents can call directly.&lt;/p&gt;

&lt;p&gt;This article covers what it is, how to try it quickly, and whether it is worth your time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea in one paragraph
&lt;/h2&gt;

&lt;p&gt;Most "GUI" applications are really a user interface sitting on top of an engine. Blender has &lt;code&gt;bpy&lt;/code&gt;. LibreOffice can run headless. Shotcut and Kdenlive render through MLT and &lt;code&gt;melt&lt;/code&gt;. Inkscape works on SVG. CLI-Anything analyzes an application's codebase, finds that underlying engine, and generates a Python CLI (built on Click) that exposes the real functionality as commands. Every generated CLI supports &lt;code&gt;--json&lt;/code&gt; output for agents, a human-readable mode for you, an interactive REPL, and project state with undo/redo.&lt;/p&gt;

&lt;p&gt;So instead of an agent clicking through menus, it runs something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cli-anything-gimp project new &lt;span class="nt"&gt;--width&lt;/span&gt; 1920 &lt;span class="nt"&gt;--height&lt;/span&gt; 1080 &lt;span class="nt"&gt;-o&lt;/span&gt; poster.json
cli-anything-gimp &lt;span class="nt"&gt;--json&lt;/span&gt; layer add &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"Background"&lt;/span&gt; &lt;span class="nt"&gt;--type&lt;/span&gt; solid &lt;span class="nt"&gt;--color&lt;/span&gt; &lt;span class="s2"&gt;"#1a1a2e"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Structured input, structured output, no screenshots.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two ways to use it
&lt;/h2&gt;

&lt;p&gt;The project has two distinct entry points, and it helps to know which one you want before you start.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You want to...&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Use CLIs that already exist (GIMP, Blender, LibreOffice, OBS, Zotero, n8n, and many more)&lt;/td&gt;
&lt;td&gt;CLI-Hub, the package manager&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generate a new CLI for software that is not covered yet, including your own internal tools&lt;/td&gt;
&lt;td&gt;The CLI-Anything plugin for your coding agent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most developers should start with the first one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 1: Install ready-made CLIs with CLI-Hub
&lt;/h2&gt;

&lt;p&gt;You need Python 3.10 or newer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;cli-anything-hub

&lt;span class="c"&gt;# See what is available&lt;/span&gt;
cli-hub list
cli-hub search image

&lt;span class="c"&gt;# Install one and inspect it&lt;/span&gt;
cli-hub &lt;span class="nb"&gt;install &lt;/span&gt;gimp
cli-hub info gimp
cli-hub launch gimp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One thing that trips people up: these CLIs wrap the real software. If you install the Blender CLI, you still need Blender installed on your machine. The CLI is the interface, not a replacement for the application.&lt;/p&gt;

&lt;h3&gt;
  
  
  Let your agent pick the tool
&lt;/h3&gt;

&lt;p&gt;If your agent supports skills (Claude Code, Codex, OpenClaw, and others), you can install a "meta-skill" that teaches it to search CLI-Hub, install what it needs, and read the usage docs on its own:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add HKUDS/CLI-Anything &lt;span class="nt"&gt;--skill&lt;/span&gt; cli-hub-meta-skill &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then just describe the task:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Find appropriate CLI software in CLI-Hub and complete the task: convert
all .docx files in ./reports to PDF and merge them into one file.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each generated CLI also ships with a &lt;code&gt;SKILL.md&lt;/code&gt; file describing its commands, so the agent gets documentation in a format it understands without you writing any prompts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 2: Generate a CLI for something new
&lt;/h2&gt;

&lt;p&gt;This is the more ambitious part of the project. You point the plugin at a codebase (a local folder or a GitHub URL), and your coding agent runs a seven-phase pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Analyze&lt;/strong&gt; the source and map GUI actions to backend APIs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design&lt;/strong&gt; command groups, state model, and output formats&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implement&lt;/strong&gt; the Click CLI with REPL, JSON output, and undo/redo&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan tests&lt;/strong&gt; in a &lt;code&gt;TEST.md&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write tests&lt;/strong&gt;, both unit and end-to-end against the real software&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document&lt;/strong&gt; the results&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publish&lt;/strong&gt; a &lt;code&gt;setup.py&lt;/code&gt; so the CLI installs onto your PATH&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In Claude Code, setup looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/plugin marketplace add HKUDS/CLI-Anything
/plugin &lt;span class="nb"&gt;install &lt;/span&gt;cli-anything

/cli-anything ./gimp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also pass a repo URL, for example &lt;code&gt;/cli-anything https://github.com/blender/blender&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When the first pass is done, the refine command looks for gaps between what the software can do and what the CLI covers, then adds commands and tests for them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/cli-anything:refine ./gimp &lt;span class="s2"&gt;"batch processing and filters"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then install and use the result like any Python package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;gimp/agent-harness &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
cli-anything-gimp &lt;span class="nt"&gt;--help&lt;/span&gt;
cli-anything-gimp          &lt;span class="c"&gt;# no arguments opens the REPL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The plugin is available for Claude Code, Cursor, Codex, OpenCode, GitHub Copilot CLI, Pi, Qodercli, OpenClaw, Hermes, and Reasonix. Several of these are marked experimental or community-maintained, so expect the Claude Code path to be the most polished.&lt;/p&gt;

&lt;p&gt;A note for Windows users: Claude Code runs shell commands through &lt;code&gt;bash&lt;/code&gt;, so install Git for Windows or use WSL, otherwise you will hit &lt;code&gt;cygpath: command not found&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it is genuinely useful
&lt;/h2&gt;

&lt;p&gt;A few use cases where this approach makes a lot of sense:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Media and document pipelines.&lt;/strong&gt; Batch converting documents with LibreOffice, rendering scenes in Blender, cutting and captioning video. These are tasks agents previously could not do reliably at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrapping messy APIs.&lt;/strong&gt; You can feed API docs or an SDK to the generator and get one stateful CLI instead of dozens of raw HTTP calls. Fewer tool calls usually means fewer tokens and fewer mistakes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Internal tools.&lt;/strong&gt; If your team has an internal app with a decent codebase but no API, this is a fast way to make it scriptable for both agents and humans.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing and benchmarks.&lt;/strong&gt; Once software has a CLI, you can generate agent tasks and evaluators in code rather than through screen recordings.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The repo's demos are worth a look. One shows an agent building a full HTTPS handshake diagram in Draw.io purely through commands. Another shows an agent assembling a rover model in FreeCAD with a live preview updating at each step.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest caveats
&lt;/h2&gt;

&lt;p&gt;The project is popular (over 51k stars at the time of writing) and moves fast, but go in with realistic expectations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generation is not free.&lt;/strong&gt; The seven-phase pipeline is a long agent run: reading a large codebase, writing an implementation, then writing and running tests. On a big project like Blender, that is a lot of tokens and time. Treat the output as a solid first draft that you review, not a finished product.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Coverage depends on what was exposed.&lt;/strong&gt; A CLI is only as good as the commands it has. When a task needs something the harness does not expose, the agent cannot fall back to "just clicking it" the way a GUI agent could. This is a real trade-off: structured interfaces are more reliable when they cover the task and more brittle when they do not. The refine command helps, but it is not magic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quality varies across the registry.&lt;/strong&gt; The core harnesses (GIMP, Blender, LibreOffice, Inkscape, Audacity, and others) have substantial test suites, and the repo reports more than 2,000 passing tests overall. Newer community contributions are thinner. Check the test count and last update before relying on a specific one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You still need the real software.&lt;/strong&gt; This is a feature, since it means you get real rendering and not a toy reimplementation, but it also means setup involves installing the upstream app and sometimes configuring it (for example, an API key or a local server).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mind what you install.&lt;/strong&gt; Like any community registry, review what a CLI does before giving an agent permission to run it, especially ones that touch credentials, file systems, or remote services. The maintainers have been fixing issues like path injection and unsafe XML parsing, which is a good sign, but it also tells you the attack surface is real.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, is it worth a try?
&lt;/h2&gt;

&lt;p&gt;Yes, with a clear scope.&lt;/p&gt;

&lt;p&gt;If your agent work touches anything outside the code editor, such as documents, media, diagrams, or a desktop app your team relies on, CLI-Hub is a low-effort win. A &lt;code&gt;pip install&lt;/code&gt;, one &lt;code&gt;cli-hub install&lt;/code&gt;, and your agent can do things it simply could not do before. That takes about ten minutes to evaluate.&lt;/p&gt;

&lt;p&gt;The generator is worth trying if you have a specific tool you want agents to control and you are willing to review and refine the output. It is less appealing if you just want to experiment casually, because the token and time cost of a full generation run is real.&lt;/p&gt;

&lt;p&gt;The bigger idea behind the project is also worth paying attention to: rather than making agents better at pretending to be humans in front of a screen, give software an interface designed for agents in the first place. Whether or not CLI-Anything becomes the standard way to do that, it is a practical, working example of the approach today.&lt;/p&gt;

&lt;p&gt;A good first experiment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;cli-anything-hub
cli-hub search office
cli-hub &lt;span class="nb"&gt;install &lt;/span&gt;libreoffice
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then ask your agent to convert and merge a folder of documents, and compare how that feels against doing it through GUI automation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Links&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repository: &lt;a href="https://github.com/HKUDS/CLI-Anything" rel="noopener noreferrer"&gt;github.com/HKUDS/CLI-Anything&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>cli</category>
      <category>agents</category>
      <category>opensource</category>
    </item>
    <item>
      <title>TokenSpeed: A New LLM Inference Engine Built for Coding Agents.</title>
      <dc:creator>ArshTechPro</dc:creator>
      <pubDate>Thu, 15 Oct 2026 22:42:25 +0000</pubDate>
      <link>https://dev.to/arshtechpro/tokenspeed-a-new-llm-inference-engine-built-for-coding-agents-34jc</link>
      <guid>https://dev.to/arshtechpro/tokenspeed-a-new-llm-inference-engine-built-for-coding-agents-34jc</guid>
      <description>&lt;p&gt;If you self-host large language models, you have probably settled into one of two camps. You use vLLM or SGLang because they are easy to run, or you use TensorRT-LLM because it squeezes the most out of NVIDIA hardware and you are willing to pay for that in setup pain.&lt;/p&gt;

&lt;p&gt;TokenSpeed wants to end that trade-off. It is an open-source (MIT) inference engine from the LightSeek Foundation that targets TensorRT-LLM-level performance with vLLM-level usability, and it is tuned specifically for agentic workloads like coding assistants.&lt;/p&gt;

&lt;p&gt;In this post we will cover what it is, why "agentic" changes the problem, how to get a server running in a few minutes, and whether it deserves a spot in your stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  What problem is it solving?
&lt;/h2&gt;

&lt;p&gt;Most inference benchmarks look like chat: a short prompt, a short answer, done. Coding agents look nothing like that.&lt;/p&gt;

&lt;p&gt;A typical agent session has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Very long contexts.&lt;/strong&gt; Prompts regularly pass 50K tokens once you include files, tool output, and history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Many turns.&lt;/strong&gt; The same conversation comes back dozens of times, each time slightly longer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A hard speed floor.&lt;/strong&gt; If tokens stream slowly, the agent feels broken. The TokenSpeed team targets around 70 tokens per second per user as a minimum, sometimes 200+.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the goal is not just "maximum throughput." It is maximum throughput per GPU &lt;em&gt;while every user still gets fast generation&lt;/em&gt;. That framing drives most of TokenSpeed's design choices.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works (the short version)
&lt;/h2&gt;

&lt;p&gt;You do not need to understand the internals to use it, but four pieces are worth knowing about:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Modeling layer.&lt;/strong&gt; Instead of hand-writing tensor-parallel communication code for every model, developers annotate where data should live at module boundaries, and a small static compiler generates the collective operations. Less custom parallelism code means fewer bugs when adding new models.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scheduler.&lt;/strong&gt; The control plane is written in C++ as a finite-state machine. Request lifecycle and KV cache ownership are modeled explicitly, and the type system catches unsafe KV cache reuse at compile time instead of at 3 a.m. in production. The execution side stays in Python so it is still easy to hack on.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Kernels.&lt;/strong&gt; Kernels live in their own pluggable package with a registry, so NVIDIA and AMD backends can sit side by side. Their MLA (Multi-head Latent Attention) kernel for Blackwell is fast enough that vLLM has adopted it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Entrypoint.&lt;/strong&gt; Requests come in through an OpenAI-compatible server built on their SMG project, keeping CPU-side overhead low.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What you need
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;An NVIDIA GPU host (CUDA 13 for the nightly wheels), or an AMD host with ROCm 7.2&lt;/li&gt;
&lt;li&gt;Python 3.11 to 3.13&lt;/li&gt;
&lt;li&gt;Docker with GPU support if you want the easy path&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Option 1: Docker (recommended)
&lt;/h3&gt;

&lt;p&gt;The project ships a runner image with everything preinstalled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker pull lightseekorg/tokenspeed-runner:latest

docker run &lt;span class="nt"&gt;-itd&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--shm-size&lt;/span&gt; 32g &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--gpus&lt;/span&gt; all &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ipc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;host &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--network&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;host &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; ~/.cache:/home/runner/.cache &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; tokenspeed &lt;span class="se"&gt;\&lt;/span&gt;
  lightseekorg/tokenspeed-runner:latest &lt;span class="se"&gt;\&lt;/span&gt;
  /bin/bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mounting your cache directory means model weights survive container restarts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 2: pip (nightly wheels)
&lt;/h3&gt;

&lt;p&gt;On a CUDA 13 machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--upgrade&lt;/span&gt; tokenspeed &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--extra-index-url&lt;/span&gt; https://lightseek.org/whl/nightly
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nightlies are versioned by date, so you can pin one for reproducibility:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"tokenspeed==0.1.0.post20260930"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--extra-index-url&lt;/span&gt; https://lightseek.org/whl/nightly
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AMD users should follow the ROCm instructions in the &lt;a href="https://lightseek.org/tokenspeed/guides/getting-started" rel="noopener noreferrer"&gt;getting started guide&lt;/a&gt;, since you need to install ROCm PyTorch first and use a separate wheel index.&lt;/p&gt;

&lt;p&gt;Check that everything is in place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tokenspeed &lt;span class="nb"&gt;env
&lt;/span&gt;tokenspeed serve &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Launch your first model
&lt;/h3&gt;

&lt;p&gt;GPT-OSS 20B fits on a single GPU and is a good smoke test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tokenspeed serve openai/gpt-oss-20b &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--served-model-name&lt;/span&gt; gpt-oss-20b &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--host&lt;/span&gt; 0.0.0.0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--port&lt;/span&gt; 8000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tensor-parallel-size&lt;/span&gt; 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Call it like OpenAI
&lt;/h3&gt;

&lt;p&gt;Because the server speaks the OpenAI API, your existing client code works with only a &lt;code&gt;base_url&lt;/code&gt; change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EMPTY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8000/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-oss-20b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Explain KV caching in two sentences.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or with curl:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:8000/v1/chat/completions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "model": "gpt-oss-20b",
    "messages": [{"role": "user", "content": "Hello!"}],
    "max_tokens": 100
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole loop. Anything that already talks to OpenAI, including most agent frameworks, can point at this server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Going bigger
&lt;/h3&gt;

&lt;p&gt;For large MoE models the flags will look familiar if you have used vLLM. Here is the shape of a Kimi K2.5 launch on four Blackwell GPUs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tokenspeed serve nvidia/Kimi-K2.5-NVFP4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--served-model-name&lt;/span&gt; kimi-k2.5 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--trust-remote-code&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-model-len&lt;/span&gt; 262144 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--kv-cache-dtype&lt;/span&gt; fp8 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--quantization&lt;/span&gt; nvfp4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tensor-parallel-size&lt;/span&gt; 4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-expert-parallel&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--reasoning-parser&lt;/span&gt; kimi_k25 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tool-call-parser&lt;/span&gt; kimik2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--host&lt;/span&gt; 0.0.0.0 &lt;span class="nt"&gt;--port&lt;/span&gt; 8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://lightseek.org/tokenspeed/recipes/models" rel="noopener noreferrer"&gt;model recipes page&lt;/a&gt; has tested launch commands for DeepSeek V4, Qwen3 and Qwen3.8, GLM 5.x, MiniMax M3, Kimi K3, Nemotron-3 Super, and more. Start from a recipe rather than tuning flags from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about performance?
&lt;/h2&gt;

&lt;p&gt;The team's headline numbers, all self-reported:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On Kimi K2.5 running on NVIDIA B200 with a real coding-agent trace (SWE-smith, evaluated with EvalScope), TokenSpeed beat TensorRT-LLM across the whole latency/throughput curve. That works out to about 9% faster at batch size 1 and about 11% more throughput at around 100 tokens per second per user.&lt;/li&gt;
&lt;li&gt;Their MLA decode kernel cut latency roughly in half versus TensorRT-LLM's on typical speculative-decoding workloads.&lt;/li&gt;
&lt;li&gt;They reported up to 580 tokens per second on Qwen3.5-397B-A17B for agentic workloads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are vendor benchmarks, so treat them as a reason to test, not as a guarantee. The good news is the benchmark harness is in the repo under &lt;code&gt;test/agentic_benchmark&lt;/code&gt;, so you can reproduce it on your own traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is it worth a try?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Yes, if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You serve large MoE models (DeepSeek, Kimi, Qwen, GLM, MiniMax) for coding agents or other long-context, multi-turn workloads.&lt;/li&gt;
&lt;li&gt;You run on recent NVIDIA hardware (Hopper, and especially Blackwell) or AMD MI350-class GPUs, and you care about cost per token.&lt;/li&gt;
&lt;li&gt;You have been eyeing TensorRT-LLM for speed but do not want its build and setup overhead.&lt;/li&gt;
&lt;li&gt;You want day-zero support for new frontier open models. TokenSpeed shipped launch-day support for Kimi K3 and Thinking Machines' Inkling.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/lightseekorg/tokenspeed" rel="noopener noreferrer"&gt;lightseekorg/tokenspeed&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>llm</category>
      <category>programming</category>
    </item>
    <item>
      <title>REA: Let Your AI Agent Reverse Engineer the App Features You Wish You Had</title>
      <dc:creator>ArshTechPro</dc:creator>
      <pubDate>Thu, 08 Oct 2026 11:28:11 +0000</pubDate>
      <link>https://dev.to/arshtechpro/rea-let-your-ai-agent-reverse-engineer-the-app-features-you-wish-you-had-3en7</link>
      <guid>https://dev.to/arshtechpro/rea-let-your-ai-agent-reverse-engineer-the-app-features-you-wish-you-had-3en7</guid>
      <description>&lt;p&gt;You are using an app and notice a feature that just works. Instant offline search. A clever sync behavior. A smooth update flow. You want something similar in your own product, but you have no source code, no docs, and no idea where to start.&lt;/p&gt;

&lt;p&gt;Traditionally, the answer was "learn reverse engineering." That means picking a disassembler, learning its API, digging through strings and symbols, following call graphs by hand, and copying findings between tools. It is a deep skill, and most application developers never have the time to pick it up.&lt;/p&gt;

&lt;p&gt;REA takes a different approach: it hands that work to your AI coding agent.&lt;/p&gt;

&lt;p&gt;In this post we will cover what REA is, how it works, how to set it up, and whether it is worth your time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is REA?
&lt;/h2&gt;

&lt;p&gt;REA is an open-source (MIT) tool, written in TypeScript, that gives AI agents a consistent way to investigate software. It ships as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A CLI&lt;/strong&gt; (&lt;code&gt;rea&lt;/code&gt;) you can run from your terminal&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An MCP server&lt;/strong&gt; that agents like Claude Code, Cursor, Codex, Gemini CLI, Windsurf, and Claude Desktop can call directly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pitch is simple: point your agent at an app, ask how a feature works, and the agent uses REA to inspect the app, explain the feature with evidence, and then build a version adapted to your stack.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo8qri1jzrdmw14novgbk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo8qri1jzrdmw14novgbk.png" alt="REA running an analysis inside Hopper" width="799" height="455"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model: Decompile, Understand, Recreate
&lt;/h2&gt;

&lt;p&gt;REA frames every investigation in three stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Decompile&lt;/strong&gt; - open the app and recover readable code, strings, symbol names, and other clues.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Understand&lt;/strong&gt; - follow the code from one part of the app to another until the agent can explain how the feature actually works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recreate&lt;/strong&gt; - turn what was learned into a feature for your own product, in your language and framework.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One thing worth stating up front: REA does not claim to recover the original source code, and it does not clone apps. It gives the agent pseudocode, assembly, symbols, strings, and relationships, and it shows the evidence behind every conclusion.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a real investigation looks like
&lt;/h2&gt;

&lt;p&gt;Say you ask your agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Reverse engineer the Notes app. Find how offline search works, explain it,
and build a version for my project using TypeScript and SQLite.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Behind the scenes, the agent works through roughly these steps using REA tools:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What the agent does&lt;/th&gt;
&lt;th&gt;Example REA tools&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Opens and identifies the binary&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;open_binary&lt;/code&gt;, &lt;code&gt;binary_overview&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Looks for search-related clues&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;search_strings&lt;/code&gt;, &lt;code&gt;search_procedures&lt;/code&gt;, &lt;code&gt;list_names&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Connects clues to actual code&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;find_xrefs_to_name&lt;/code&gt;, &lt;code&gt;xrefs&lt;/code&gt;, &lt;code&gt;procedure_callers&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Rebuilds the control flow&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;get_call_graph&lt;/code&gt;, &lt;code&gt;procedure_callees&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Decompiles the important functions&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;procedure_pseudo_code&lt;/code&gt;, &lt;code&gt;batch_decompile&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Writes the feature in your project&lt;/td&gt;
&lt;td&gt;Your agent's normal editing and test tools&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;REA handles steps 1 through 5. Step 6 is your agent doing what it already does well: writing code.&lt;/p&gt;

&lt;p&gt;If you have done any manual reverse engineering, you will recognize this workflow. The difference is that the agent drives it instead of you.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works under the hood
&lt;/h2&gt;

&lt;p&gt;REA itself is not a disassembler. It is an orchestration layer that sits between your agent and real analysis tools.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Agent / Terminal
      |
      v
  REA (CLI + MCP server)
      |
      +--&amp;gt; Deep analysis providers: Hopper, Ghidra
      +--&amp;gt; Native macOS utilities (Mach-O, code signatures, plists, Swift demangling)
      +--&amp;gt; Artifact provider (ZIP, APK, IPA, ASAR, MSIX inventory)
      +--&amp;gt; Browser provider (passive Chrome DevTools Protocol observation)
      +--&amp;gt; Process capture provider (controlled terminal sessions)
      |
      v
  Evidence records you can export, compare, and reuse
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few design choices stand out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Providers do the heavy lifting.&lt;/strong&gt; Hopper is the main deep-analysis provider, and Ghidra is supported as a read-only, bring-your-own option on Linux (plus an experimental Windows x64 mode).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything produces evidence.&lt;/strong&gt; Each successful result is stored as a structured "Evidence v2" record that includes the provider, confidence, and known limitations. You can export these bundles, import them later, and diff them across app versions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unknowns stay unknown.&lt;/strong&gt; REA is strict about not presenting guesses as facts. Static inference and runtime observation are labeled separately, and incomplete data is reported as incomplete.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local by design.&lt;/strong&gt; There is no hosted analysis service. The app you analyze stays on your machine (though your AI model provider still has its own data policy).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same engine for CLI and MCP.&lt;/strong&gt; Whatever your agent can do, you can also do from the terminal.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it can analyze today
&lt;/h2&gt;

&lt;p&gt;Native binaries get the deepest support, but REA covers more than that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Native apps&lt;/strong&gt;: Mach-O, ELF, PE, and macOS &lt;code&gt;.app&lt;/code&gt; bundles, including Swift and Objective-C metadata&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Packages and archives&lt;/strong&gt;: ZIP, APK, IPA, ASAR, plus file-level inventory without extraction&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Electron and JavaScript apps&lt;/strong&gt;: static mapping of entry points, modules, IPC channels, preload scripts, and source maps, without executing anything&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Websites&lt;/strong&gt;: passive observation of a browser you already control, over a local Chrome DevTools endpoint&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.NET (managed PE/CLI)&lt;/strong&gt;: execution-free triage of metadata, members, and native boundaries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version diffs&lt;/strong&gt;: compare two releases of an app and get a report of what changed&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Requirements
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;macOS 12+, or Ubuntu 24.04+, Fedora 41+, or 64-bit Arch Linux (Windows x64 is experimental and Ghidra-only)&lt;/li&gt;
&lt;li&gt;Node.js 22.19+ or 24.11+&lt;/li&gt;
&lt;li&gt;A deep-analysis provider: Hopper (setup can install it, and the free demo mode works) or your own Ghidra 12.1.2 with JDK 21&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Install and run setup
&lt;/h3&gt;

&lt;p&gt;The quickest path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;--yes&lt;/span&gt; rea-agents@latest setup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or install the CLI globally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--global&lt;/span&gt; rea-agents
rea setup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setup is careful. It detects which agents you have installed, asks which capabilities you want, shows the exact files it will change, and defaults to "No" before applying anything. Use &lt;code&gt;rea setup --dry-run&lt;/code&gt; if you want to preview the plan first.&lt;/p&gt;

&lt;p&gt;When setup finishes, restart your agent so it picks up the new MCP server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check your environment
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rea doctor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells you whether something is missing, misconfigured, or unsupported on your machine.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try it from the terminal
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rea analyze /Applications/Notes.app
rea search /Applications/Notes.app &lt;span class="s2"&gt;"offline"&lt;/span&gt;
rea trace /Applications/Notes.app &lt;span class="s2"&gt;"offline"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Or just ask your agent
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Understand how search works in the Notes app, show me the evidence,
and build a similar feature for my project.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Manual MCP configuration
&lt;/h3&gt;

&lt;p&gt;If your agent supports local MCP servers but is not auto-detected, add this to its config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rea"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rea-agents@3.1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pin to the latest published version from &lt;a href="https://www.npmjs.com/package/rea-agents" rel="noopener noreferrer"&gt;npm&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Safety and permissions
&lt;/h2&gt;

&lt;p&gt;Reverse-engineering tools can be risky if they run code you do not trust. REA leans heavily toward caution:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dynamic features (process capture, browser observation, runtime inspection) are &lt;strong&gt;disabled by default&lt;/strong&gt; and need explicit environment variables plus per-call approval.&lt;/li&gt;
&lt;li&gt;File-based evidence commands only work inside directories you approve, using variables like &lt;code&gt;REA_EVIDENCE_ROOTS_JSON&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Static JavaScript analysis parses code without running it.&lt;/li&gt;
&lt;li&gt;REA never calls &lt;code&gt;sudo&lt;/code&gt;, and it uses authenticated local sockets for provider communication.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The flip side: providers and launched targets still run with your user permissions. REA is not a security sandbox, so do not treat it as one when handling malware.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is it worth a try?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Yes, if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You are on macOS or Linux and already use an MCP-capable agent like Claude Code or Cursor.&lt;/li&gt;
&lt;li&gt;You want to understand how a feature works in an app you use, especially a native or Electron app.&lt;/li&gt;
&lt;li&gt;You are curious about reverse engineering but never had time to learn Hopper or Ghidra.&lt;/li&gt;
&lt;li&gt;You need to document an undocumented file format, protocol, or interface for interoperability.&lt;/li&gt;
&lt;li&gt;You maintain software and want to diff behavior across versions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Maybe hold off if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You are on Windows. Support there is experimental and limited.&lt;/li&gt;
&lt;li&gt;You want a quick, zero-config tool. REA has a lot of opt-in switches and environment variables, which is good for safety but takes time to learn.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;A few practical notes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hopper is separate commercial software. The demo mode works, but has vendor-defined limits.&lt;/li&gt;
&lt;li&gt;Deep investigations can use a lot of agent tokens, so watch your model costs.&lt;/li&gt;
&lt;li&gt;Respect the law and the software license. Reverse engineering for learning and interoperability is common, but some license agreements and jurisdictions restrict it. Recreate ideas and behavior, not copyrighted code or assets.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;REA turns reverse engineering from a specialist skill into something you can delegate to your coding agent. You describe the feature, the agent investigates the app, shows you how it reached its conclusions, and then helps you build your own version.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/morluto/rea" rel="noopener noreferrer"&gt;github.com/morluto/rea&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>reverseengineering</category>
      <category>ai</category>
      <category>mcp</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
