<?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>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>
    <item>
      <title>Trying e2e: Write End-to-End Tests with Plain English and Real Assertions</title>
      <dc:creator>ArshTechPro</dc:creator>
      <pubDate>Tue, 06 Oct 2026 13:27:49 +0000</pubDate>
      <link>https://dev.to/arshtechpro/trying-e2e-write-end-to-end-tests-with-plain-english-and-real-assertions-2p7i</link>
      <guid>https://dev.to/arshtechpro/trying-e2e-write-end-to-end-tests-with-plain-english-and-real-assertions-2p7i</guid>
      <description>&lt;p&gt;End-to-end tests are valuable and painful in equal measure. Selectors break when someone renames a class. Mobile tests need their own tooling. A flow that takes one sentence to describe takes fifty lines to script.&lt;/p&gt;

&lt;p&gt;e2e is an open source framework that tries to fix this by mixing two styles in one test: plain-English steps run by an agent, and normal locators and assertions you already know. Here is what it does and whether it deserves a spot in your stack.&lt;/p&gt;

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

&lt;p&gt;e2e is an end-to-end testing framework for web and mobile apps. You describe a goal in natural language, and an agent interacts with the app to complete it. When you need exact checks, you use regular locators and assertions in the same test.&lt;/p&gt;

&lt;p&gt;It is built by the TesterArmy team and released under the Apache-2.0 license.&lt;/p&gt;

&lt;h2&gt;
  
  
  A first look
&lt;/h2&gt;

&lt;p&gt;This is what a test looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// tests/checkout.e2e.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&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;e2e&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&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 member upgrades to Pro&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;app&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/settings/billing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;act&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;upgrade the workspace to the Pro plan&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;the invoice preview shows a prorated amount&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toContainText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Pro&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;p&gt;There are three kinds of steps here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;app.open&lt;/code&gt; navigates to a page.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;agent.act&lt;/code&gt; and &lt;code&gt;agent.assert&lt;/code&gt; take plain-English instructions and checks.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;expect(screen.getByRole(...))&lt;/code&gt; is a precise, deterministic assertion.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent handles the part that is tedious to script, such as clicking through a billing flow. Your own assertion verifies the result. The test reads like a description of the feature, and you still get an exact check at the end.&lt;/p&gt;

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

&lt;p&gt;Setup is one command:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;From there, the docs at &lt;a href="https://e2e.tester.army/docs" rel="noopener noreferrer"&gt;e2e.tester.army/docs&lt;/a&gt; cover the quickstart, writing tests, mobile, and the full reference. The &lt;code&gt;e2e&lt;/code&gt; package also ships every docs page in &lt;code&gt;node_modules/e2e/docs&lt;/code&gt;, so coding agents can read them offline.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that matters: replay
&lt;/h2&gt;

&lt;p&gt;The obvious worry with AI-driven tests is cost and flakiness. Calling a model on every run is slow and expensive, and results can vary.&lt;/p&gt;

&lt;p&gt;e2e's answer is replay. According to the README, when an agent step is verified by a later assertion, the framework records the actions it took. The next run replays them with no model calls, until the app changes. Tests without agent steps need no model at all.&lt;/p&gt;

&lt;p&gt;In practice this means you pay for the model when a flow is new or has changed, not on every CI run. You can also bring your own subscription, API key, or local model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Packages
&lt;/h2&gt;

&lt;p&gt;e2e is split into small packages, so you only install what you need:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;e2e&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SDK, runner, and CLI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@e2e-dev/web&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Browser engine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@e2e-dev/mobile&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;iOS and Android engine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@e2e-dev/github&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pull request comment reporter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@e2e-dev/kernel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hosted browsers for the web engine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@e2e-dev/eas&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hosted iOS simulators and Android emulators&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you are coming from another tool, the docs include migration guides for Playwright, Cypress, Selenium, Detox, and Maestro.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is it worth trying?
&lt;/h2&gt;

&lt;p&gt;It is a good fit if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have a critical flow (checkout, onboarding, billing) that is expensive to script and easy to break.&lt;/li&gt;
&lt;li&gt;You test mobile and want something simpler than a separate toolchain.&lt;/li&gt;
&lt;li&gt;You like the idea of readable tests but still want exact assertions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Be aware of a few things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The project is young, so expect rough edges and changes. Start with one or two flows rather than migrating a whole suite.&lt;/li&gt;
&lt;li&gt;It comes from a company that also sells a testing platform. The framework is open source, but keep that context in mind.&lt;/li&gt;
&lt;li&gt;Agent steps depend on a model, so check how replay behaves on your own app before trusting it in CI.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Verdict
&lt;/h2&gt;

&lt;p&gt;e2e is a practical take on AI in testing: use the agent for the tedious parts, use real assertions for the checks that matter, and replay recorded runs so you are not paying for a model on every build.&lt;/p&gt;

&lt;p&gt;Try it on one flow this week. If it saves you from maintaining a brittle script, expand from there.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/tester-army/e2e" rel="noopener noreferrer"&gt;github.com/tester-army/e2e&lt;/a&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>javascript</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Caveman: Make Your AI Coding Agent Talk Less (and Save Tokens)</title>
      <dc:creator>ArshTechPro</dc:creator>
      <pubDate>Fri, 02 Oct 2026 14:22:14 +0000</pubDate>
      <link>https://dev.to/arshtechpro/caveman-make-your-ai-coding-agent-talk-less-and-save-tokens-4moi</link>
      <guid>https://dev.to/arshtechpro/caveman-make-your-ai-coding-agent-talk-less-and-save-tokens-4moi</guid>
      <description>&lt;p&gt;If you use an AI coding agent every day, you have probably noticed how much of its output is padding. Phrases like "The reason this is happening is likely because..." or "I'd recommend that you consider..." appear before the useful part of the answer. You pay for those tokens, and you spend time reading them.&lt;/p&gt;

&lt;p&gt;Caveman is an open source project that fixes this. Its tagline is "why use many token when few do trick", and it does what it says: your agent answers in short, blunt sentences without the filler.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;Here is the example from the README. The question is why a React component keeps re-rendering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Normal agent (69 tokens):&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The reason your React component is re-rendering is likely because you're creating a new object reference on each render cycle. When you pass an inline object as a prop, React's shallow comparison sees it as a different object every time, which triggers a re-render. I'd recommend using useMemo to memoize the object.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Caveman agent (19 tokens):&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;New object ref each render. Inline object prop = new ref = re-render. Wrap in &lt;code&gt;useMemo&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The diagnosis and the fix are the same. Only the throat-clearing is gone.&lt;/p&gt;

&lt;p&gt;Code, commands, file paths, and exact error messages are never rewritten. Only the prose around them gets shortened, so the answers stay accurate and copy-pasteable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install in one command
&lt;/h2&gt;

&lt;p&gt;The simplest version is a skill, which is a rule file your agent loads:&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 JuliusBrussee/caveman
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works with 30+ agents, including Claude Code, Codex, Gemini CLI, Cursor, Windsurf, Cline, and Copilot. If your agent does not pick it up automatically, type &lt;code&gt;/caveman&lt;/code&gt; in the chat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Intensity levels and commands
&lt;/h2&gt;

&lt;p&gt;You control how terse the agent gets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/caveman lite
/caveman full
/caveman ultra
/caveman off
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The skill also adds a few useful commands:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/caveman-commit&lt;/code&gt; writes short Conventional Commit messages.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/caveman-review&lt;/code&gt; gives one-line, actionable code review comments.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/caveman-compress &amp;lt;file&amp;gt;&lt;/code&gt; shrinks Markdown memory files and keeps a backup of the original.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/caveman-stats&lt;/code&gt; shows local token usage and estimated savings in Claude Code.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;The author ran ten ordinary coding prompts through the Claude API, with and without the skill. Output tokens dropped by 65% on average. The range is wide, though:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Normal&lt;/th&gt;
&lt;th&gt;Caveman&lt;/th&gt;
&lt;th&gt;Saved&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Implement React error boundary&lt;/td&gt;
&lt;td&gt;3454&lt;/td&gt;
&lt;td&gt;456&lt;/td&gt;
&lt;td&gt;87%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Set up PostgreSQL connection pool&lt;/td&gt;
&lt;td&gt;2347&lt;/td&gt;
&lt;td&gt;380&lt;/td&gt;
&lt;td&gt;84%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Explain git rebase vs merge&lt;/td&gt;
&lt;td&gt;702&lt;/td&gt;
&lt;td&gt;292&lt;/td&gt;
&lt;td&gt;58%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refactor callback to async/await&lt;/td&gt;
&lt;td&gt;387&lt;/td&gt;
&lt;td&gt;301&lt;/td&gt;
&lt;td&gt;22%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pattern is simple. When the agent would have written an essay, you save a lot. When the answer was mostly code, you save very little.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read this before quoting the 65%
&lt;/h2&gt;

&lt;p&gt;The project is unusually honest about its limits, and you should be too:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The skill only shortens &lt;strong&gt;output&lt;/strong&gt;. Input and reasoning tokens do not change.&lt;/li&gt;
&lt;li&gt;The skill's own rules add roughly 1 to 1.5k input tokens on every turn.&lt;/li&gt;
&lt;li&gt;Whole-session savings will be lower than the benchmark table, and on work that was already terse you can end up paying more.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The README says speed and readability are the real product, and the discount is a bonus. Measure your own setup before promising anyone a 65% cut on the bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  The optional proxy
&lt;/h2&gt;

&lt;p&gt;There is also a second, bigger piece: a local proxy that sits between your agent and the AI provider. It shrinks what the agent reads, such as logs, diffs, and test output, before each call, and it keeps a backup of every original on your machine.&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; @caveman-ai/cli &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; caveman setup &lt;span class="nt"&gt;--install&lt;/span&gt;
caveman claude
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a 54-run Claude Code benchmark, total input tokens dropped by about 33%. One case, an HTML dashboard, actually got 9.9% worse, and the README leaves that row in the table. It is a bigger commitment than the skill, so start with the skill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things to know before you install
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Telemetry:&lt;/strong&gt; the &lt;code&gt;caveman&lt;/code&gt; CLI sends anonymous usage stats by default (commands run and token counts, never prompts or code). Turn it off with &lt;code&gt;caveman telemetry off&lt;/code&gt; or &lt;code&gt;DO_NOT_TRACK=1&lt;/code&gt;. The skill itself runs entirely on your machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Licensing:&lt;/strong&gt; the skill and CLI are MIT. The engine and proxy are BSL-1.1, which is source-available but not OSI open source. Self-hosting for your own use is fine.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Is it worth trying?
&lt;/h2&gt;

&lt;p&gt;Yes, with realistic expectations. The skill is a one-line install, easy to turn off, and does not touch your code. If your agent is wordy, you get faster, easier-to-scan answers right away. Treat any token savings as a bonus and check them with &lt;code&gt;/caveman-stats&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/JuliusBrussee/caveman" rel="noopener noreferrer"&gt;github.com/JuliusBrussee/caveman&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>llm</category>
      <category>agents</category>
    </item>
    <item>
      <title>Impeccable: Stop Your AI Coding Agent From Building the Same Ugly UI</title>
      <dc:creator>ArshTechPro</dc:creator>
      <pubDate>Fri, 02 Oct 2026 06:45:50 +0000</pubDate>
      <link>https://dev.to/arshtechpro/impeccable-stop-your-ai-coding-agent-from-building-the-same-ugly-ui-2pg4</link>
      <guid>https://dev.to/arshtechpro/impeccable-stop-your-ai-coding-agent-from-building-the-same-ugly-ui-2pg4</guid>
      <description>&lt;p&gt;Impeccable is a design skill for AI coding agents. It started from Anthropic's frontend-design skill and extends it with three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A single setup step.&lt;/strong&gt; &lt;code&gt;/impeccable init&lt;/code&gt; inspects your project and writes a &lt;code&gt;PRODUCT.md&lt;/code&gt; file with your audience, purpose, constraints, and voice. Later commands use it as context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;24 commands&lt;/strong&gt; that give you and the agent a shared design vocabulary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;61 deterministic detector rules&lt;/strong&gt; that flag common AI design mistakes. They run without an LLM or an API key.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It works with Claude Code, Cursor, Codex CLI, Gemini CLI, GitHub Copilot, and a long list of other tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install in two commands
&lt;/h2&gt;

&lt;p&gt;From your 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;npx impeccable &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The installer detects which AI tools you use and asks whether to install for the project or globally. Reload your tool, then run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/impeccable init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you use Claude Code, you can also install it as a plugin with &lt;code&gt;/plugin marketplace add pbakaus/impeccable&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The commands worth knowing
&lt;/h2&gt;

&lt;p&gt;You do not need all 24. These cover most daily use:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&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;&lt;code&gt;/impeccable craft&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Plan, then build a new surface with visual iteration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/impeccable critique&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;UX review of hierarchy, clarity, and tone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/impeccable audit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Technical checks for accessibility, performance, and responsiveness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/impeccable polish&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Final pass before shipping&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/impeccable distill&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Strip a design down to what matters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/impeccable harden&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Error states, text overflow, i18n, edge cases&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each command takes a target, so you stay in control of scope:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/impeccable audit the header
/impeccable polish the checkout form
/impeccable harden checkout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also describe what you want in plain language, for example &lt;code&gt;/impeccable redo this hero section&lt;/code&gt;. If you use one command a lot, &lt;code&gt;/impeccable pin audit&lt;/code&gt; turns it into a standalone &lt;code&gt;/audit&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that works without AI: the detector
&lt;/h2&gt;

&lt;p&gt;This is the feature I would try first. The CLI can scan your code, or a live URL, for design problems with no model involved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx impeccable detect src/
npx impeccable detect https://example.com
npx impeccable detect &lt;span class="nt"&gt;--json&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It catches things like purple gradients, bounce easing, tiny touch targets, skipped headings, and cramped padding. The &lt;code&gt;--json&lt;/code&gt; flag and exit codes make it easy to add to CI. Because the rules are deterministic, the results are repeatable, which is hard to get from asking an AI to review the design.&lt;/p&gt;

&lt;p&gt;The README is honest about the limits too: a clean run is evidence, not proof, and it does not replace looking at the page on real viewports.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design hook
&lt;/h2&gt;

&lt;p&gt;On Claude Code, Cursor, Codex, GitHub Copilot, and Grok Build, the installer can add a hook that runs the detector whenever the agent edits a UI file. The agent sees the findings and can fix them in the same session. On Cursor it can block a bad write before it lands. This catches problems while the code is being written, instead of in review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things to check before adopting
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hooks run on their own.&lt;/strong&gt; In Claude Code, installed hooks run independently of tool approval, and the first run may download a binary into &lt;code&gt;~/.impeccable/bin/&lt;/code&gt;. Review the hook before using it in unattended runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live mode is for local development only.&lt;/strong&gt; Do not inject it into a deployed production site.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignore the right files.&lt;/strong&gt; The repo provides a &lt;code&gt;.gitignore&lt;/code&gt; block for the &lt;code&gt;.impeccable/&lt;/code&gt; folder, so screenshots and caches do not get committed. Keep the shared config and design files tracked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Opinionated by design.&lt;/strong&gt; It discourages fonts like Inter and Arial and warns against nested cards. If your brand uses Inter, you can add an ignore rule with a reason:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx impeccable ignores add-value overused-font Inter &lt;span class="nt"&gt;--reason&lt;/span&gt; &lt;span class="s2"&gt;"Brand font"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Is it worth trying?
&lt;/h2&gt;

&lt;p&gt;Yes, for most developers who build UI with AI tools. The setup takes a couple of minutes, nothing locks you in, and you can try the detector alone without letting an AI touch your code.&lt;/p&gt;

&lt;p&gt;It is most useful if you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ship side projects or MVPs and do not have a designer&lt;/li&gt;
&lt;li&gt;are tired of every AI-generated page looking the same&lt;/li&gt;
&lt;li&gt;want automated design checks in CI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/pbakaus/impeccable" rel="noopener noreferrer"&gt;github.com/pbakaus/impeccable&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>opensource</category>
      <category>frontend</category>
    </item>
    <item>
      <title>NVIDIA OpenShell Explained: A Safer Runtime for AI Agents</title>
      <dc:creator>ArshTechPro</dc:creator>
      <pubDate>Wed, 30 Sep 2026 11:53:47 +0000</pubDate>
      <link>https://dev.to/arshtechpro/nvidia-openshell-explained-a-safer-runtime-for-ai-agents-3kcn</link>
      <guid>https://dev.to/arshtechpro/nvidia-openshell-explained-a-safer-runtime-for-ai-agents-3kcn</guid>
      <description>&lt;p&gt;AI agents are getting genuinely useful. They can read your files, install packages, call APIs, and run shell commands. That is exactly what makes them risky.&lt;/p&gt;

&lt;p&gt;If you have ever pasted an API key into an agent's environment and then held your breath while it ran, this article is for you. NVIDIA has an open source project called &lt;strong&gt;OpenShell&lt;/strong&gt; that tries to solve this problem. Let's look at what it is, how it works, and whether it is worth your time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem in one paragraph
&lt;/h2&gt;

&lt;p&gt;An agent is only useful if it can do things. But every capability you give it is also a capability that can go wrong: a prompt injection in a web page, a hallucinated &lt;code&gt;rm -rf&lt;/code&gt;, a dependency that phones home, or a leaked token in a log. Most of us handle this today by running the agent in a Docker container and hoping for the best. That helps, but a container by default does not know the difference between "download a package from PyPI" and "send your credentials to a random server."&lt;/p&gt;

&lt;h2&gt;
  
  
  What OpenShell is
&lt;/h2&gt;

&lt;p&gt;According to the project, OpenShell is "the safe, private runtime for autonomous AI agents." In practical terms, it is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;CLI&lt;/strong&gt; you use to create and manage sandboxes.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;gateway&lt;/strong&gt; that acts as the control plane for sandboxes, policies, and access.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;sandbox&lt;/strong&gt; where the agent actually runs, isolated from your host.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;policy system&lt;/strong&gt; where you declare what the agent can access.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You write the rules. OpenShell enforces them. The agent does not get to negotiate.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;The README describes two main ideas.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Enforcement at the kernel level
&lt;/h3&gt;

&lt;p&gt;Each agent runs in its own isolated sandbox. Kernel-level controls restrict which files the agent can open and which system calls it can make. Every network connection goes through a policy check before it leaves the sandbox.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Formally verified policy changes
&lt;/h3&gt;

&lt;p&gt;Agents often need new access as they work. Maybe your agent suddenly wants to reach a new host. Normally you would either approve everything (unsafe) or approve each request by hand (exhausting).&lt;/p&gt;

&lt;h2&gt;
  
  
  A mental model that helps
&lt;/h2&gt;

&lt;p&gt;Think of it like a building with a security desk.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;sandbox&lt;/strong&gt; is the room the agent works in.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;policy&lt;/strong&gt; is the visitor list and the set of doors the agent's badge opens.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;provider&lt;/strong&gt; is a mail clerk who attaches the real stamp (your credential) to an approved letter, so the agent never holds the stamp itself.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;gateway&lt;/strong&gt; is the security office that manages all of it.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Requirements from the README: Linux, macOS on Apple Silicon, or Windows with WSL 2 (marked experimental). You also need Docker, Podman, or host virtualization.&lt;/p&gt;

&lt;p&gt;Install and create a sandbox:&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;-LsSf&lt;/span&gt; https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh
openshell sandbox create &lt;span class="nt"&gt;--name&lt;/span&gt; demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The installer sets up the CLI and a local gateway. The default sandbox image is a minimal Ubuntu with no agent installed, so you are starting with a clean, locked-down box.&lt;/p&gt;

&lt;p&gt;As always with a &lt;code&gt;curl | sh&lt;/code&gt; installer, read the script before running it on a machine you care about.&lt;/p&gt;

&lt;p&gt;To run a real agent, the docs have a "Run Your First Agent" walkthrough. It runs OpenCode against a free OpenRouter model and shows how to approve new access as the agent asks for it. That walkthrough is the best way to see the policy loop in action.&lt;/p&gt;

&lt;h2&gt;
  
  
  Talking to it from code
&lt;/h2&gt;

&lt;p&gt;If you want to integrate OpenShell into your own application, there are SDKs that connect to a gateway:&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;Install&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;uv add openshell&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm install @nvidia/openshell-sdk&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;&lt;code&gt;go get github.com/NVIDIA/OpenShell/sdk/go@latest&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;cargo add openshell-sdk --git ... --tag &amp;lt;release-tag&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Note that the SDKs do not install the CLI. You still need a gateway running, and the project recommends using the same release for both.&lt;/p&gt;

&lt;p&gt;There is also a set of "agent skills" you can install so your coding agent learns how to drive the OpenShell CLI and write policies 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;npx skills add NVIDIA/OpenShell
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Other things worth knowing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kubernetes support.&lt;/strong&gt; You can deploy the gateway with Helm. Your CNI must enforce &lt;code&gt;NetworkPolicy&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extensibility.&lt;/strong&gt; The docs mention middleware, interceptors, and compute drivers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU support.&lt;/strong&gt; The sandbox docs cover GPUs, which matters if your agents run local models.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License.&lt;/strong&gt; Apache 2.0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Telemetry.&lt;/strong&gt; Anonymous telemetry is on by default and limited to operational categories and counts. The README says it does not collect prompts, file paths, credentials, or model names. You can turn it off with &lt;code&gt;OPENSHELL_TELEMETRY_ENABLED=false&lt;/code&gt; on the gateway.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Reasons to try it
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;It targets a real problem.&lt;/strong&gt; Credential leakage and over-permissioned agents are the most common ways agent setups go wrong. Keeping real secrets out of the agent's reach is a strong design choice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Policy as configuration.&lt;/strong&gt; Being able to declare "this agent can read this folder and call these two APIs" and have it enforced is far better than relying on prompt instructions like "please do not touch anything else."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good signs of a serious project.&lt;/strong&gt; The repository has around 10k stars, 1.4k forks, and over 1,500 commits. It ships SDKs in four languages, has an RFC process, a governance document, and a security policy. This is not a weekend demo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It comes from NVIDIA.&lt;/strong&gt; That is not a guarantee of quality, but it does suggest ongoing investment and attention to GPU-heavy agent workloads.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;If you are...&lt;/th&gt;
&lt;th&gt;Should you try it?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Running autonomous agents with real credentials or access to sensitive data&lt;/td&gt;
&lt;td&gt;Yes. This is the target use case.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Building an agent platform or internal tooling for a team&lt;/td&gt;
&lt;td&gt;Yes. The policy and gateway model fits well.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Experimenting with agents on your laptop, mostly for fun&lt;/td&gt;
&lt;td&gt;Maybe. A plain container may be enough for now.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Building a simple chatbot with no tool access&lt;/td&gt;
&lt;td&gt;No. You do not need it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Needing production stability today with no tolerance for change&lt;/td&gt;
&lt;td&gt;Wait a bit. Watch the releases and let it mature.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;My short answer: &lt;strong&gt;yes, it is worth an afternoon of your time&lt;/strong&gt;, especially if you are giving agents access to anything you would be upset to lose. Follow the "Run Your First Agent" guide, watch how the policy approvals work, and then decide whether the extra structure pays for itself in your setup. Just go in knowing it is a young project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Repository: &lt;a href="https://github.com/NVIDIA/OpenShell" rel="noopener noreferrer"&gt;github.com/NVIDIA/OpenShell&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>devops</category>
      <category>security</category>
    </item>
  </channel>
</rss>
