<?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: douzatan</title>
    <description>The latest articles on DEV Community by douzatan (@douzatan).</description>
    <link>https://dev.to/douzatan</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%2F3500763%2Fd477d43f-7280-40d1-b783-a04e4503cb67.jpeg</url>
      <title>DEV Community: douzatan</title>
      <link>https://dev.to/douzatan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/douzatan"/>
    <language>en</language>
    <item>
      <title>Cursor Windsurf Mobile Agent Skill: Delegating Android UI Tasks From Your IDE</title>
      <dc:creator>douzatan</dc:creator>
      <pubDate>Thu, 15 Oct 2026 01:45:32 +0000</pubDate>
      <link>https://dev.to/douzatan/cursor-windsurf-mobile-agent-skill-delegating-android-ui-tasks-from-your-ide-njj</link>
      <guid>https://dev.to/douzatan/cursor-windsurf-mobile-agent-skill-delegating-android-ui-tasks-from-your-ide-njj</guid>
      <description>&lt;p&gt;Installing a &lt;strong&gt;cursor windsurf mobile agent skill&lt;/strong&gt; lets your AI code editor delegate live Android UI verification, deep-link testing, and recurring in-app workflows to a cloud or physical Android device without stuffing raw XML view hierarchies into your editor context window. Modern AI IDEs excel at editing TypeScript backends, refactoring Kotlin view models, and running headless browser checks, yet they stall the moment a feature has to be verified inside a real mobile application.&lt;/p&gt;

&lt;p&gt;By wiring &lt;a href="https://airtap.ai/" rel="noopener noreferrer"&gt;Airtap&lt;/a&gt; into Cursor Agent or Windsurf Cascade through a lightweight skill definition, your IDE coding assistant can dispatch high-level mobile goals to an isolated cloud Android phone or a local test handset running the Autopilot app, then read the structured step log right inside your workspace.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Your IDE Need a Cursor Windsurf Mobile Agent Skill Instead of Raw ADB Scripts?
&lt;/h2&gt;

&lt;p&gt;Your IDE needs a &lt;strong&gt;cursor windsurf mobile agent skill&lt;/strong&gt; because forcing an editor-based coding model to drive an Android screen through raw shell commands wastes tens of thousands of context tokens per turn and breaks whenever custom UI frameworks omit static resource identifiers. When full-stack or mobile engineers try to connect an AI code editor directly to a local Android emulator, they usually start by telling the agent to run shell commands through the &lt;a href="https://developer.android.com/tools/adb" rel="noopener noreferrer"&gt;Android Debug Bridge (adb) command guide&lt;/a&gt; (verified October 2026) and dump the view hierarchy using the &lt;a href="https://developer.android.com/training/testing/other-components/ui-automator" rel="noopener noreferrer"&gt;Android UI Automator testing framework&lt;/a&gt; (verified October 2026).&lt;/p&gt;

&lt;p&gt;In practice, that raw loop creates three immediate bottlenecks inside your editor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Context Window Exhaustion&lt;/strong&gt;: A single &lt;code&gt;adb shell uiautomator dump&lt;/code&gt; on a modern commercial Android app produces 2,500 to 5,000 lines of deeply nested XML nodes. Feeding that dump into Cursor Composer or Windsurf Cascade on every tap consumes 20,000 to 40,000 tokens, pushing your actual source code out of the active context window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom Canvas and WebView Blindness&lt;/strong&gt;: Many production mobile screens mix Jetpack Compose, Flutter canvases, React Native bridges, or embedded WebViews that do not expose clean &lt;code&gt;resource-id&lt;/code&gt; strings. Coordinate-guessing scripts fail whenever a promotional banner shifts a button down by 80 pixels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local Emulator RAM Overhead&lt;/strong&gt;: Running a full Android Studio AVD alongside Docker containers, a local backend server, and an AI IDE routinely pegs 16 GB to 32 GB of laptop memory and stops working the moment your machine sleeps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Delegating the mobile UI loop through a dedicated skill separates &lt;strong&gt;code intelligence in the IDE&lt;/strong&gt; from &lt;strong&gt;screen perception and actuation on the Android device&lt;/strong&gt;. Your editor sends a concise task contract, and the mobile runtime handles the &lt;code&gt;Observe -&amp;gt; Interpret -&amp;gt; Decide -&amp;gt; Act&lt;/code&gt; cycle on the phone screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Do Raw Local Emulators Compare to Skill-Delegated Android Execution?
&lt;/h2&gt;

&lt;p&gt;Before adding the skill files to your repository, review how local ADB scripting compares against delegating Android tasks through a dedicated mobile agent skill:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engineering Dimension&lt;/th&gt;
&lt;th&gt;Raw Local &lt;code&gt;adb&lt;/code&gt; + &lt;code&gt;uiautomator dump&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;Local Instrumented Espresso / Maestro&lt;/th&gt;
&lt;th&gt;Skill-Delegated Android Runtime (Airtap)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IDE Context Token Cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Very high (20k to 40k tokens per screen XML dump)&lt;/td&gt;
&lt;td&gt;Moderate (requires maintaining separate test selector files)&lt;/td&gt;
&lt;td&gt;Low (IDE sends goal + receiver ID, receives concise JSON outcome)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Local Hardware Footprint&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Heavy (Android SDK + local emulator consuming 4 to 8 GB RAM)&lt;/td&gt;
&lt;td&gt;Heavy (local emulator or paid CI device farm required)&lt;/td&gt;
&lt;td&gt;Zero local emulator overhead when targeting a Cloud Android Phone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dynamic Popups &amp;amp; Custom Views&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Brittle (breaks on unexpected permission modals or missing IDs)&lt;/td&gt;
&lt;td&gt;Fails unless every popup is explicitly scripted in advance&lt;/td&gt;
&lt;td&gt;Handled dynamically by live screen perception on each step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Target Device Options&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Local USB-tethered device or local AVD only&lt;/td&gt;
&lt;td&gt;Local AVD or CI test runner only&lt;/td&gt;
&lt;td&gt;Cloud Android Phone container or physical phone via Autopilot app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2FA, Login &amp;amp; Payment Screens&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Requires hardcoded test credentials in plain text&lt;/td&gt;
&lt;td&gt;Requires staging auth-bypass flags in app build&lt;/td&gt;
&lt;td&gt;Pauses automatically and sends a live &lt;code&gt;Take Control&lt;/code&gt; link to human&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This separation is particularly useful when you are building a backend webhook, deep-link router, or mobile API endpoint in your IDE and want to verify how the real Android app responds without switching windows.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Do You Install and Configure the Skill in Cursor and Windsurf?
&lt;/h2&gt;

&lt;p&gt;Setting up the integration takes less than five minutes and uses standard workspace configuration files supported by both editors.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Install the skill package and Personal Access Token
&lt;/h3&gt;

&lt;p&gt;You can install the skill from the &lt;code&gt;airtap-ai/airtap-skill&lt;/code&gt; repository (also listed on ClawHub) using &lt;code&gt;npx skills&lt;/code&gt;, or pull the machine-readable specification directly via &lt;code&gt;curl&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Option A: Install directly for Cursor or Windsurf via npx skills&lt;/span&gt;
npx skills add airtap-ai/airtap-skill &lt;span class="nt"&gt;-a&lt;/span&gt; cursor
npx skills add airtap-ai/airtap-skill &lt;span class="nt"&gt;-a&lt;/span&gt; windsurf

&lt;span class="c"&gt;# Option B: Fetch the raw skill specification into your workspace skills folder&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; .agents/skills/airtap
curl &lt;span class="nt"&gt;-sL&lt;/span&gt; https://airtap.ai/skill.md &lt;span class="nt"&gt;-o&lt;/span&gt; .agents/skills/airtap/SKILL.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, open the Airtap web app, navigate to &lt;strong&gt;Settings -&amp;gt; Personal Access Token&lt;/strong&gt;, generate a token, and export it in your shell profile (never commit tokens to Git):&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;export &lt;/span&gt;&lt;span class="nv"&gt;AIRTAP_PAT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"atp_live_your_personal_access_token_here"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the skill installed, your IDE agent gains six core operations: listing available receivers (devices) or models, creating mobile tasks, monitoring running tasks, sending follow-up input when a task asks a clarifying question, canceling tasks, and updating device location context.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Configure project rules for Cursor Agent
&lt;/h3&gt;

&lt;p&gt;As detailed in the &lt;a href="https://docs.cursor.com/context/rules-for-ai" rel="noopener noreferrer"&gt;Cursor Rules for AI documentation&lt;/a&gt; (verified October 2026), Cursor loads modular project instructions from &lt;code&gt;.cursor/rules/&lt;/code&gt;. Create &lt;code&gt;.cursor/rules/airtap-mobile.mdc&lt;/code&gt; so Cursor Agent knows when and how to invoke the mobile skill instead of running raw &lt;code&gt;adb&lt;/code&gt; commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Cursor Rule: .cursor/rules/airtap-mobile.mdc&lt;/span&gt;
&lt;span class="gh"&gt;# Scope: Mobile UI verification, deep-link checks, and Android routine delegation&lt;/span&gt;

When a task requires inspecting or interacting with an Android mobile app:
&lt;span class="p"&gt;1.&lt;/span&gt; Read &lt;span class="sb"&gt;`.agents/skills/airtap/SKILL.md`&lt;/span&gt; before proposing any mobile shell commands.
&lt;span class="p"&gt;2.&lt;/span&gt; Do NOT run raw &lt;span class="sb"&gt;`adb shell uiautomator dump`&lt;/span&gt; or hardcode X/Y tap coordinates.
&lt;span class="p"&gt;3.&lt;/span&gt; Verify that &lt;span class="sb"&gt;`$AIRTAP_PAT`&lt;/span&gt; is set in the environment, then list available receivers (Cloud Android Phone or Autopilot physical device).
&lt;span class="p"&gt;4.&lt;/span&gt; Formulate a concise task contract specifying:
&lt;span class="p"&gt;   -&lt;/span&gt; Target app name and starting screen or deep link
&lt;span class="p"&gt;   -&lt;/span&gt; Exact observation or UI verification goal
&lt;span class="p"&gt;   -&lt;/span&gt; Stop condition (pause and request human Take Control on any login, 2FA, or payment commit screen)
&lt;span class="p"&gt;5.&lt;/span&gt; Poll the task status until completion and summarize the step-by-step action log in the editor response.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Configure workspace rules for Windsurf Cascade
&lt;/h3&gt;

&lt;p&gt;In Windsurf, the agentic Cascade engine reads workspace-level instructions from &lt;code&gt;.windsurfrules&lt;/code&gt;, as documented in the &lt;a href="https://docs.windsurf.com/windsurf/cascade" rel="noopener noreferrer"&gt;Windsurf Cascade workspace documentation&lt;/a&gt; (verified October 2026). Add a dedicated mobile delegation block to your repository's &lt;code&gt;.windsurfrules&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Windsurf Cascade Rule (.windsurfrules) - Mobile Execution Bridge&lt;/span&gt;

&lt;span class="gu"&gt;## Mobile App Verification &amp;amp; Routine Delegation&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Whenever verifying backend API changes, push notifications, or deep links against an Android app, delegate UI execution via &lt;span class="sb"&gt;`.agents/skills/airtap/SKILL.md`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; Prefer the isolated Cloud Android Phone receiver for general regression and staging checks.
&lt;span class="p"&gt;-&lt;/span&gt; Select the Autopilot physical device receiver when testing SIM-bound, carrier-specific, or local-network hardware apps.
&lt;span class="p"&gt;-&lt;/span&gt; Never place app passwords, OTP codes, or payment credentials inside prompts or environment files. If a task enters &lt;span class="sb"&gt;`PAUSED_TAKE_CONTROL`&lt;/span&gt;, notify the developer to complete authentication via the live link.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keeping both &lt;code&gt;.cursor/rules/airtap-mobile.mdc&lt;/code&gt; and &lt;code&gt;.windsurfrules&lt;/code&gt; committed to version control alongside &lt;code&gt;.agents/skills/airtap/SKILL.md&lt;/code&gt; ensures that every engineer on your team gets identical mobile delegation behavior regardless of which AI code editor they prefer. When a pull request modifies an API response schema or an Android navigation graph, either editor can immediately run the same mobile verification contract without per-developer emulator setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Do You Dispatch Live Android Tasks and Handle Interactive Interrupts From the Editor?
&lt;/h2&gt;

&lt;p&gt;Once your workspace rules are in place, you can ask Cursor Agent or Windsurf Cascade to verify mobile flows directly from your coding session. First, decide which receiver fits your task:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cloud Android Phone&lt;/strong&gt;: Provisioned in about 60 seconds inside a private, isolated cloud container. You can sign into Google Play to install staging or production apps, view the live screen stream in the web dashboard, and let sessions wipe cleanly on logout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Physical Android Phone via Autopilot&lt;/strong&gt;: Install the Autopilot Android app on a spare handset on your desk to let the agent operate apps bound to a physical SIM card, local Wi-Fi IoT devices (such as a Nest thermostat), or device-specific logins.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you prompt your IDE agent to verify a change (for example, &lt;em&gt;"After deploying the updated promo endpoint, open our Android app on the cloud phone, apply code OCTSAVE20 on the cart screen, and confirm the discount badge renders without submitting payment"&lt;/em&gt;), the agent uses the skill workflow to list receivers, create the task, and monitor progress:&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;"operation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"create_task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"receiver_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;"rcv_cloud_android_primary"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"task_prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Open the staging retail app, add the default test item to cart, enter promo code OCTSAVE20, and read the discount line item. Stop before checkout payment."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stop_on_sensitive_screen"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"location_override"&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;"city"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"San Jose, CA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"latitude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;37.3382&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"longitude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;-121.8863&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;During execution, the runtime runs its &lt;code&gt;Observe -&amp;gt; Interpret -&amp;gt; Decide -&amp;gt; Act&lt;/code&gt; loop on the Android screen and returns structured status updates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;If the task is ambiguous&lt;/strong&gt;: Suppose the promo code field is hidden behind a collapsed accordion or displays two different tier options. The agent pauses to ask a clarifying question, and your IDE can pass your answer back using the skill's follow-up input call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If the app prompts for login or 2FA&lt;/strong&gt;: Passwords are never stored or visible to the agent, and the runtime is cryptographically blocked from reading secure fields. The task pauses and emits a &lt;code&gt;Take Control&lt;/code&gt; notification (also accessible via iMessage, SMS, or Telegram) so you can sign in directly on the isolated container screen and resume the run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If the workflow should run on a recurring schedule&lt;/strong&gt;: Once an interactive run succeeds, you can save it as a scheduled routine in the visual routine builder so it runs automatically every morning or after every nightly build.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Best Practices Keep IDE-to-Mobile Delegation Fast and Reliable?
&lt;/h2&gt;

&lt;p&gt;After wiring the skill into your repository, follow these four engineering habits to keep your IDE sessions fast and predictable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Write Goal-Oriented Task Contracts, Not Pixel Scripts&lt;/strong&gt;: Tell the mobile agent &lt;em&gt;what&lt;/em&gt; state to verify (&lt;code&gt;"Open Orders tab, select the topmost order, and verify the status badge text"&lt;/code&gt;), not &lt;em&gt;how&lt;/em&gt; many pixels to scroll. The screen-reading loop adapts to layout density and popups automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set Explicit Stop Boundaries in Every Prompt&lt;/strong&gt;: Always state where the mobile agent must stop (for example, &lt;code&gt;"Stop on the order review screen; do not tap Place Order"&lt;/code&gt;). Keeping irreversible actions behind human confirmation prevents accidental test orders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Location Updates for Geo-Fenced App Flows&lt;/strong&gt;: If your mobile app renders different pricing, store inventory, or booking slots based on GPS coordinates, use the skill's location update action before creating the verification task.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Promote Proven Checks to Scheduled Routines&lt;/strong&gt;: Rather than re-running routine mobile smoke checks manually from your editor every morning, convert working tasks into scheduled routines that alert you on Telegram, SMS, or iMessage only when a check fails or a target condition hits.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Airtap is free to start, with a daily soft usage cap that resets at 00:00 UTC, making it easy to test both one-off IDE verifications and scheduled mobile routines without provisioning local emulator farms.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: Assisted by AI tooling (&lt;code&gt;#abotwrotethis&lt;/code&gt;).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cursor</category>
      <category>windsurf</category>
      <category>android</category>
      <category>ai</category>
    </item>
    <item>
      <title>Building a Multi Agent Workflow Telegram to Slack Relay With a Cloud Android Phone</title>
      <dc:creator>douzatan</dc:creator>
      <pubDate>Wed, 14 Oct 2026 13:53:14 +0000</pubDate>
      <link>https://dev.to/douzatan/building-a-multi-agent-workflow-telegram-to-slack-relay-with-a-cloud-android-phone-3885</link>
      <guid>https://dev.to/douzatan/building-a-multi-agent-workflow-telegram-to-slack-relay-with-a-cloud-android-phone-3885</guid>
      <description>&lt;p&gt;Wiring a multi agent workflow telegram to slack pipeline lets an on-call engineer or operations lead send a short command in Telegram, delegate a live mobile app inspection to an isolated cloud Android phone, and publish a structured status card into a team Slack channel. Most tutorials that connect Telegram to Slack stop at a trivial text forwarder: a webhook receives a string in one chat app and reposts the exact same string in another. Real operational workflows are rarely that simple. When a growth, logistics, or marketplace ops team needs to verify regional in-app promo pricing, check live delivery or rideshare wait times, or inspect a smart-facility controller that only ships as an Android or iOS binary, standard HTTP webhook glue hits a brick wall.&lt;/p&gt;

&lt;p&gt;To bridge that gap without maintaining brittle reverse-engineered mobile scrapers, you can equip your orchestrator agent with &lt;a href="https://airtap.ai/" rel="noopener noreferrer"&gt;Airtap&lt;/a&gt;, which provides a real Android phone inside an isolated cloud container (or connects to a spare physical Android phone via Autopilot) that developer agents can drive through a lightweight skill file. Your ingress bot handles the Telegram command, your orchestrator delegates the screen-level mobile task to the Android runtime, and your egress worker formats the structured output into Slack Block Kit.&lt;/p&gt;

&lt;p&gt;This tutorial walks through the four-node relay architecture, webhook deduplication and security headers, delegating mobile UI checks via the skill specification, formatting Slack Block Kit cards, and handling session expiration or human-in-the-loop escalations. API details below are verified against official Telegram Bot API and Slack Developer documentation as of October 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a multi agent workflow telegram to slack relay needs a real Android runtime
&lt;/h2&gt;

&lt;p&gt;In a typical operations setup, Telegram serves as the fast, mobile-first command trigger for founders or on-call responders who are away from their desks, while Slack serves as the persistent system of record where the wider engineering or operations team tracks incident timelines and price audits. A multi agent workflow telegram to slack architecture becomes much more useful when the middle of the pipeline can inspect software that has no public web API.&lt;/p&gt;

&lt;p&gt;Consider three everyday ops checks that fail inside a standard headless browser worker:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Verifying app-only retail and delivery pricing.&lt;/strong&gt; Regional delivery apps, pharmacy loyalty wallets, and travel platforms frequently display different prices or promo banners inside their native mobile apps than on their public websites.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checking live local supply and wait times.&lt;/strong&gt; Mobility, courier, and booking apps lock real-time ETA maps behind mutual TLS (mTLS) and Google Play Integrity attestation, blocking raw Python &lt;code&gt;requests&lt;/code&gt; or headless Chromium scripts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inspecting mobile-bound hardware and IoT controls.&lt;/strong&gt; Many smart thermostats, access locks, and building sensors only expose their status dashboards inside a companion Android app.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Splitting the relay into four single-responsibility nodes keeps your codebase modular:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Node 1 (Telegram Ingress Webhook):&lt;/strong&gt; Validates the incoming operator command, checks the sender allowlist, deduplicates the &lt;code&gt;update_id&lt;/code&gt;, and returns HTTP &lt;code&gt;200 OK&lt;/code&gt; in under 500 milliseconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node 2 (Orchestrator Agent):&lt;/strong&gt; Parses the user's intent, loads the mobile execution skill, and dispatches a bounded, read-only mobile inspection task with a strict JSON return schema.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node 3 (Cloud Android Runtime):&lt;/strong&gt; Opens the genuine Android application inside an isolated container, reads the live screen and accessibility tree, and extracts the requested metrics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node 4 (Slack Block Kit Egress):&lt;/strong&gt; Converts the structured JSON telemetry into an actionable Slack card posted to &lt;code&gt;#ops-mobile-checks&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 1: Verify and deduplicate incoming Telegram Bot webhook commands
&lt;/h2&gt;

&lt;p&gt;The biggest operational trap when connecting a chat webhook to a real UI agent is webhook timeout retry storms. Under the official &lt;a href="https://core.telegram.org/bots/api#setwebhook" rel="noopener noreferrer"&gt;Telegram Bot API &lt;code&gt;setWebhook&lt;/code&gt; specification&lt;/a&gt;, Telegram expects your HTTPS endpoint to acknowledge incoming &lt;code&gt;Update&lt;/code&gt; payloads promptly, and it will re-deliver the same &lt;code&gt;update_id&lt;/code&gt; if your server hangs for 45 seconds waiting for an Android app to launch and render.&lt;/p&gt;

&lt;p&gt;Always verify the &lt;code&gt;X-Telegram-Bot-Api-Secret-Token&lt;/code&gt; header, check &lt;code&gt;update_id&lt;/code&gt; against a short-lived cache or Redis set, acknowledge the request immediately, and push the agent job to an asynchronous background worker:&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&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;seenUpdates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;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;ALLOWED_USER_IDS&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="mi"&gt;10492819&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;29481022&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;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&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="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/webhooks/telegram&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-Telegram-Bot-Api-Secret-Token&lt;/span&gt;&lt;span class="dl"&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;secret&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TELEGRAM_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="p"&gt;)&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unauthorized&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;update&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&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;update&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;update_id&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;seenUpdates&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;update&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;update_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;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;deduped&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;seenUpdates&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;update&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;update_id&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;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&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;msg&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ALLOWED_USER_IDS&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;msg&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="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;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ignored&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;queued&lt;/span&gt;&lt;span class="p"&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;void&lt;/span&gt; &lt;span class="nf"&gt;runMultiAgentRelay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&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;msg&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="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&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;By returning &lt;code&gt;200 OK&lt;/code&gt; before calling &lt;code&gt;runMultiAgentRelay&lt;/code&gt;, your pipeline guarantees that a 40-second mobile screen check will never trigger three duplicate runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Equip your orchestrator agent with the cloud Android skill
&lt;/h2&gt;

&lt;p&gt;Instead of writing custom Appium scripts or brittle coordinate macros for every mobile app you want to inspect, your orchestrator agent (such as Claude Code, Codex, OpenClaw, or a custom agent loop) can load Airtap's public skill definition directly.&lt;/p&gt;

&lt;p&gt;On your agent runner host, fetch the skill specification so your orchestrator knows how to dispatch tasks to your cloud Android phone (or to a spare physical Android phone connected via Autopilot):&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;-sL&lt;/span&gt; https://airtap.ai/skill.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the orchestrator delegates a task to the mobile execution layer, instruct it to enforce two rules: keep the operation strictly read-only unless a human explicitly confirms a state change, and return a deterministic JSON object so your downstream Slack formatter never has to regex-parse conversational prose.&lt;/p&gt;

&lt;p&gt;Here is an example task prompt template that the orchestrator passes to the mobile worker when an operator sends &lt;code&gt;/check_ride SFO_Terminal_G&lt;/code&gt; in Telegram:&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;"task_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;"mobile_ui_inspection"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"read_only"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"instruction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Open Uber and Lyft on the signed-in Android phone. Check live pickup ETAs and standard fares from 500 Howard St, San Francisco to SFO International Terminal G. Do not tap Book or confirm any ride."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"response_schema"&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;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OK | NEEDS_TAKE_CONTROL | ERROR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"app_checked"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Uber &amp;amp; Lyft"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"uber_quote"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$42.50 (4 min ETA)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lyft_quote"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$46.10 (6 min ETA)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"requires_human_action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"take_control_reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&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;Because the cloud Android phone runs inside its own isolated container with the real apps installed, the agent reads the live screen state naturally without triggering API gateway blocks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Post structured results or escalation alerts to Slack Block Kit
&lt;/h2&gt;

&lt;p&gt;Once the mobile worker returns its structured JSON payload, the final node in your multi agent workflow telegram to slack pipeline formats the result for your team channel. According to the &lt;a href="https://api.slack.com/messaging/webhooks" rel="noopener noreferrer"&gt;Slack Incoming Webhooks documentation&lt;/a&gt;, every webhook &lt;code&gt;POST&lt;/code&gt; should include both a top-level &lt;code&gt;text&lt;/code&gt; string (used for mobile push notifications and screen readers) and a rich &lt;code&gt;blocks&lt;/code&gt; array built with the &lt;a href="https://api.slack.com/block-kit" rel="noopener noreferrer"&gt;Slack Block Kit framework&lt;/a&gt;.&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;postToSlack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&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;statusEmoji&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;OK&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="s2"&gt;🟢&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="s2"&gt;🟠&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;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;text&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;] Mobile check completed for &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;command&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="na"&gt;blocks&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="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="s2"&gt;header&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&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="s2"&gt;plain_text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;text&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;statusEmoji&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; Mobile App Check: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app_checked&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;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="s2"&gt;section&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;fields&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="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="s2"&gt;mrkdwn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`*Triggered By:*\n@&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (via Telegram)`&lt;/span&gt; &lt;span class="p"&gt;},&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="s2"&gt;mrkdwn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`*Status:*\n&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="se"&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;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="s2"&gt;mrkdwn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`*Primary Quote:*\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uber_quote&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;N/A&lt;/span&gt;&lt;span class="dl"&gt;"&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;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="s2"&gt;mrkdwn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`*Secondary Quote:*\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lyft_quote&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;N/A&lt;/span&gt;&lt;span class="dl"&gt;"&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;span class="p"&gt;},&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="s2"&gt;context&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;elements&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="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="s2"&gt;mrkdwn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Command: &lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt; · Executed on isolated Android container`&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;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SLACK_WEBHOOK_URL&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&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="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&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="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;When &lt;code&gt;result.status&lt;/code&gt; is &lt;code&gt;OK&lt;/code&gt;, the team channel gets a clean four-field summary card within seconds of the mobile check finishing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling mobile session expirations, retries, and Take Control handoffs
&lt;/h2&gt;

&lt;p&gt;Real mobile apps occasionally throw curveballs that headless scripts cannot solve on their own: a 30-day login token expires, a two-factor verification prompt appears, or an operator asks the agent to prepare a booking that requires payment confirmation.&lt;/p&gt;

&lt;p&gt;Under Airtap's security model, password fields are cryptographically blocked from agent reading, and your credentials are never stored by or visible to the AI. Instead of pasting passwords into Telegram or Slack env vars, your relay should route authentication and payment gates through a &lt;strong&gt;Take Control&lt;/strong&gt; handoff, where the operator taps a link to interact with the cloud phone screen directly, completes sign-in, and lets the agent resume.&lt;/p&gt;

&lt;p&gt;Here is how to map each failure mode across your multi agent workflow telegram to slack pipeline:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure or Escalation Scenario&lt;/th&gt;
&lt;th&gt;What Happens on the Android Runtime&lt;/th&gt;
&lt;th&gt;Orchestrator Retry Policy&lt;/th&gt;
&lt;th&gt;Slack &amp;amp; Telegram Notification Behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Telegram Webhook Timeout (&amp;gt;10s)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Task continues running normally on the Android phone&lt;/td&gt;
&lt;td&gt;Deduplicate by &lt;code&gt;update_id&lt;/code&gt;; do not spawn a second Android task&lt;/td&gt;
&lt;td&gt;ACK &lt;code&gt;200 OK&lt;/code&gt; immediately on ingress before starting the agent loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;App Session Expired / Login Prompt&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Agent detects sign-in screen; password fields are blocked from reading&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;0&lt;/code&gt; automated retries; pause task and emit &lt;code&gt;NEEDS_TAKE_CONTROL&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Post orange alert card in Slack and reply in Telegram with Take Control handoff notice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2FA Code or Passkey Challenge&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Security prompt blocks navigation to target screen&lt;/td&gt;
&lt;td&gt;Pause execution for up to 10 minutes awaiting human input&lt;/td&gt;
&lt;td&gt;Notify triggering operator in Telegram to enter the 2FA code directly via Take Control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Checkout / Payment Confirmation Gate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Agent reaches final review screen with price and terms&lt;/td&gt;
&lt;td&gt;Never auto-submit payment; hold state for explicit human confirmation&lt;/td&gt;
&lt;td&gt;Send quote summary to Slack and Telegram; require operator approval before confirming&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Transient App Modal or Promo Popup&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Promo banner overlays the main navigation bar&lt;/td&gt;
&lt;td&gt;Up to &lt;code&gt;1&lt;/code&gt; screen-level dismissal retry by the UI agent&lt;/td&gt;
&lt;td&gt;Log dismissal in context block; only alert Slack if screen remains blocked after retry&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Production checklist before deploying your relay
&lt;/h2&gt;

&lt;p&gt;Before moving your Telegram-to-Android-to-Slack relay from a local test script to an always-on team bot, verify four operational guardrails:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Lock down your Telegram sender allowlist.&lt;/strong&gt; A Telegram bot username is publicly discoverable. Checking &lt;code&gt;X-Telegram-Bot-Api-Secret-Token&lt;/code&gt; only proves the request came from Telegram's servers, not that the sender is on your team. Always filter against an explicit allowlist of numeric Telegram &lt;code&gt;from.id&lt;/code&gt; values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose the right Android device target for the job.&lt;/strong&gt; For 90% of team checks, Airtap's cloud Android phone (which sets up in about 60 seconds inside an isolated per-user container) is the cleanest choice because it supports live screen view in the web dashboard and wipes sessions cleanly on logout. If a task requires an app bound to a local office Wi-Fi network or physical SIM card, connect a spare Android phone via Autopilot (noting that live screen streaming in the web dashboard is currently supported only for the cloud phone).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep routine checks read-only by default.&lt;/strong&gt; Separate inspection commands (like &lt;code&gt;/check_price&lt;/code&gt; or &lt;code&gt;/check_eta&lt;/code&gt;) from action commands so no automated schedule or fat-fingered chat message can accidentally complete a purchase without a human confirmation step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mind your daily usage rhythm.&lt;/strong&gt; Airtap is free to start, with a daily soft usage cap, making it straightforward to prototype your multi-agent relay and run recurring team checks without provisioning custom emulator clusters.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>ai</category>
      <category>telegram</category>
      <category>slack</category>
      <category>android</category>
    </item>
    <item>
      <title>Codex Mobile App Automation: Delegating Android UI Tasks via Skill Files</title>
      <dc:creator>douzatan</dc:creator>
      <pubDate>Wed, 14 Oct 2026 13:52:11 +0000</pubDate>
      <link>https://dev.to/douzatan/codex-mobile-app-automation-delegating-android-ui-tasks-via-skill-files-4104</link>
      <guid>https://dev.to/douzatan/codex-mobile-app-automation-delegating-android-ui-tasks-via-skill-files-4104</guid>
      <description>&lt;p&gt;Setting up &lt;strong&gt;codex mobile app automation&lt;/strong&gt; lets a terminal coding agent hand off mobile-only Android UI tasks, in-app smoke tests, and recurring screen checks to a dedicated cloud or physical phone without maintaining brittle ADB shell scripts. Terminal agents are remarkably fast at refactoring TypeScript, querying Postgres, and calling REST endpoints, yet they hit an immediate wall whenever a workflow requires tapping inside a native Android application that has no public web interface or API.&lt;/p&gt;

&lt;p&gt;By connecting a terminal coding agent such as OpenAI's Codex CLI, Claude Code, Cursor, or OpenClaw to &lt;a href="https://airtap.ai/" rel="noopener noreferrer"&gt;Airtap&lt;/a&gt; through a single Markdown skill file, your desktop agent can delegate mobile UI execution to a cloud Android phone (or a local test phone running the Autopilot app) and receive structured results back in the terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why do terminal coding agents hit a wall on mobile apps, and how does codex mobile app automation solve it?
&lt;/h2&gt;

&lt;p&gt;When you run &lt;a href="https://github.com/openai/codex" rel="noopener noreferrer"&gt;OpenAI Codex CLI&lt;/a&gt; or &lt;a href="https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; (verified October 14, 2026) inside a repository, the agent has full access to your local shell, file tree, and HTTP clients. That works for backend services and web apps that can be driven through Playwright, but three common engineering and operations tasks still live exclusively inside mobile apps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mobile-only staging and release verification&lt;/strong&gt;: Verifying that a freshly built Android APK or deep-link push notification actually renders the right screen state on a real Android device after a backend deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In-app state and pricing checks&lt;/strong&gt;: Querying mobile apps that enforce certificate pinning, device attestation, or mobile-only member states that return &lt;code&gt;403 Forbidden&lt;/code&gt; to headless web scrapers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-step operational routines&lt;/strong&gt;: Automating recurring business chores (checking a mobile merchant dashboard, verifying a booking slot, or confirming an in-app delivery status) where the vendor never shipped a webhook or REST API.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Historically, giving a CLI agent access to an Android screen meant wiring up &lt;a href="https://developer.android.com/tools/adb" rel="noopener noreferrer"&gt;Android Debug Bridge (&lt;code&gt;adb&lt;/code&gt;)&lt;/a&gt;, dumping XML hierarchies with &lt;a href="https://developer.android.com/training/testing/other-components/ui-automator" rel="noopener noreferrer"&gt;Android UI Automator&lt;/a&gt;, and asking your coding LLM to parse 4,000 lines of nested &lt;code&gt;&amp;lt;node&amp;gt;&lt;/code&gt; tags on every turn. That burns context tokens, breaks whenever a Jetpack Compose or Flutter surface omits &lt;code&gt;resource-id&lt;/code&gt; attributes, and fails the moment your laptop lid closes.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;codex mobile app automation&lt;/strong&gt; setup separates the &lt;strong&gt;coding and orchestration brain&lt;/strong&gt; (running in your terminal) from the &lt;strong&gt;mobile UI execution runtime&lt;/strong&gt; (which reads the Android screen, taps, scrolls, and returns the outcome).&lt;/p&gt;

&lt;h2&gt;
  
  
  How does a declarative skill file compare to raw ADB and UI Automator scripts?
&lt;/h2&gt;

&lt;p&gt;Before wiring up a skill file, it helps to compare what happens when a terminal coding agent drives an Android device directly via &lt;code&gt;adb shell&lt;/code&gt; versus delegating the task through a dedicated mobile execution layer:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Raw &lt;code&gt;adb shell input&lt;/code&gt; + XML Dump&lt;/th&gt;
&lt;th&gt;Instrumented UI Automator / Espresso&lt;/th&gt;
&lt;th&gt;Skill-Delegated Mobile Agent (Airtap)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Setup &amp;amp; Dependencies&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Local Android SDK, USB debugging or wireless ADB, custom Python wrapper&lt;/td&gt;
&lt;td&gt;Full Gradle test target, matching APK signature, Java/Kotlin selectors&lt;/td&gt;
&lt;td&gt;Single skill file (&lt;code&gt;curl -sL https://airtap.ai/skill.md&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Token Cost in CLI Agent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Very high (15k to 40k tokens per turn for raw &lt;code&gt;uiautomator dump&lt;/code&gt; XML)&lt;/td&gt;
&lt;td&gt;Low during run, high maintenance when UI selectors change&lt;/td&gt;
&lt;td&gt;Low (CLI agent sends goal + constraints, receives concise outcome)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Custom Views / Flutter / WebView&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fails when nodes lack &lt;code&gt;resource-id&lt;/code&gt; or &lt;code&gt;content-desc&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Requires custom semantics hooks in source code&lt;/td&gt;
&lt;td&gt;Handled by hybrid visual + system UI perception loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Device Availability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Stops when developer laptop sleeps or USB disconnects&lt;/td&gt;
&lt;td&gt;Requires local emulator or paid CI device farm&lt;/td&gt;
&lt;td&gt;Runs 24/7 on Cloud Android Phone or local phone with Autopilot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Login / 2FA / Payment Screens&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Requires hardcoded test credentials or manual ADB intervention&lt;/td&gt;
&lt;td&gt;Fails or requires test-only auth bypass flags&lt;/td&gt;
&lt;td&gt;Pauses and sends a &lt;code&gt;Take Control&lt;/code&gt; link for human completion&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Instead of forcing your coding model to act as a low-level screen coordinates calculator, the skill file teaches the CLI agent how to formulate a clear task contract, dispatch it, and wait for the structured result.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you wire the Airtap skill file into Codex CLI, Claude Code, or Cursor?
&lt;/h2&gt;

&lt;p&gt;Airtap exposes a machine-readable skill specification at &lt;code&gt;https://airtap.ai/skill.md&lt;/code&gt; designed for coding agents including OpenAI Codex, Claude Code, Cursor, Windsurf, and OpenClaw.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Fetch and inspect the skill definition
&lt;/h3&gt;

&lt;p&gt;From your project root or agent skills directory, pull the official skill file:&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;# Download the Airtap mobile execution skill into your agent workspace&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; .agents/skills/airtap-mobile
curl &lt;span class="nt"&gt;-sL&lt;/span&gt; https://airtap.ai/skill.md &lt;span class="nt"&gt;-o&lt;/span&gt; .agents/skills/airtap-mobile/SKILL.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you are using OpenAI Codex CLI or Claude Code, you can also reference the skill directly in your project's &lt;code&gt;AGENTS.md&lt;/code&gt; or pass it into the session context so the agent knows when to delegate mobile UI tasks instead of writing local browser scrapers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Choose between a Cloud Android Phone and a local Autopilot device
&lt;/h3&gt;

&lt;p&gt;Depending on what your terminal agent is automating, you can bind the skill to either of two execution targets:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Airtap Cloud Phone (Default)&lt;/strong&gt;: A dedicated, isolated cloud Android instance that stays online 24/7. Ideal for scheduled checks, external app verification, and multi-app workflows that should keep running when your workstation is powered off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Physical Android Device via Autopilot&lt;/strong&gt;: Install the Airtap Autopilot app on a spare Android test phone on your desk or local Wi-Fi network. Ideal when testing local LAN builds, hardware-bound authenticators, or smart-home apps tied to your local network.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 3: Structure the task handoff from your terminal agent
&lt;/h3&gt;

&lt;p&gt;When your terminal coding agent delegates a task, the prompt payload should specify four fields so the mobile agent never guesses at success criteria:&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;task_handoff&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;target_app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Android&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;staging&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&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;target&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;mobile&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;app&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;name"&lt;/span&gt;
  &lt;span class="na"&gt;initial_state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Launch&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;app&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;from&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;home&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;screen;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;verify&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;logged-in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;workspace"&lt;/span&gt;
  &lt;span class="na"&gt;goal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Navigate&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Orders&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Filter&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;by&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Pending&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Extract&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;top&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;order&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;IDs&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;badges"&lt;/span&gt;
  &lt;span class="na"&gt;stop_conditions&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;If&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;login,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;2FA,&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;payment&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;confirmation&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;screen&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;appears,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;pause&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;request&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Take&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Control"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Return&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;structured&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;JSON&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;with&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;order_id,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;status,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;timestamp"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When invoked from a terminal session (as shown in the public "Using Airtap inside Claude Code" demonstration on &lt;code&gt;airtap.ai/see-it-in-action&lt;/code&gt;), the coding agent dispatches the instruction, Airtap's Brain and AutoPilot execute the UI navigation on the Android screen, and the CLI session receives the extracted values back to continue its local code or script execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you handle authentication, 2FA, and Take Control handoffs in a developer workflow?
&lt;/h2&gt;

&lt;p&gt;One of the worst anti-patterns in DIY mobile automation is storing plaintext passwords, TOTP seeds, or session tokens inside &lt;code&gt;.env&lt;/code&gt; files or passing them through LLM context windows.&lt;/p&gt;

&lt;p&gt;When you delegate Android UI tasks through Airtap, credential boundaries are enforced at the device container level:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Persistent isolated Android session&lt;/strong&gt;: Once you sign into an app inside your dedicated cloud Android phone (or on your own physical Android device running Autopilot), the session lives inside Android's native application sandbox rather than inside your terminal agent's prompt history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interactive &lt;code&gt;Take Control&lt;/code&gt; pause&lt;/strong&gt;: When the mobile agent encounters a login prompt, a biometric/2FA challenge, a CAPTCHA, or a checkout button, it pauses execution and sends a &lt;code&gt;Take Control&lt;/code&gt; link. You tap or type the sensitive input directly on the streamed device screen, and then hand execution back to the agent to finish the routine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read-only vs state-changing guardrails&lt;/strong&gt;: In your skill instructions, explicitly mark whether a delegated task is read-only (inspecting UI state, checking prices, reading a status screen) or allowed to submit a form.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common Failure Modes and Troubleshooting Checklist
&lt;/h2&gt;

&lt;p&gt;If your first delegated mobile task stalls or returns incomplete data, check these four common causes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Vague completion criteria&lt;/strong&gt;: Telling a mobile agent "check the app and see if it works" leads to ambiguous summaries. Always specify the exact screen element or value to return (for example, "return the exact text of the toast message or price total after applying code &lt;code&gt;TEST20&lt;/code&gt;").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing time zone or locale context&lt;/strong&gt;: If your task checks date pickers (such as flight, hotel, or appointment calendars), specify the exact date in &lt;code&gt;YYYY-MM-DD&lt;/code&gt; format and note the target time zone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local ADB conflicts on physical Autopilot phones&lt;/strong&gt;: If you are using a physical Android phone with Autopilot on your desk, disconnect active local &lt;code&gt;adb logcat&lt;/code&gt; or &lt;code&gt;scrcpy&lt;/code&gt; locks that might fight over input focus during a scheduled run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hitting daily usage caps with tight polling loops&lt;/strong&gt;: Do not write a &lt;code&gt;while true&lt;/code&gt; loop in bash that triggers a mobile UI check every 60 seconds. Use Airtap's built-in scheduling (for example, checking twice daily or every few hours) so tasks stay within the daily soft usage cap.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can I use the same skill file across OpenAI Codex CLI, Claude Code, and Cursor?
&lt;/h3&gt;

&lt;p&gt;Yes. Because &lt;code&gt;https://airtap.ai/skill.md&lt;/code&gt; is written as a portable Markdown instruction and command contract, any agentic coding tool that can read a local &lt;code&gt;.md&lt;/code&gt; skill file and execute shell or HTTP calls can use the exact same setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need to root my Android phone to use Autopilot?
&lt;/h3&gt;

&lt;p&gt;No. Connecting a physical Android phone through Airtap's Autopilot app uses standard Android system capabilities and does not require unlocking the bootloader or rooting the device. And if you use Airtap's Cloud Phone, no physical hardware is required at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does it cost to test mobile tasks from a coding agent?
&lt;/h3&gt;

&lt;p&gt;Airtap is free to start, with a daily soft usage cap that resets at 00:00 UTC, so you can wire up the skill file and test delegated Android workflows without upfront infrastructure costs.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: This tutorial was written for Airtap to demonstrate how developers connect terminal coding agents to cloud and physical Android devices via skill files. #abotwrotethis&lt;/em&gt;&lt;/p&gt;

</description>
      <category>codex</category>
      <category>android</category>
      <category>ai</category>
      <category>automation</category>
    </item>
    <item>
      <title>Claude Code Android automation without ADB: tasks, states and follow-ups</title>
      <dc:creator>douzatan</dc:creator>
      <pubDate>Tue, 29 Sep 2026 14:13:37 +0000</pubDate>
      <link>https://dev.to/douzatan/claude-code-android-automation-without-adb-tasks-states-and-follow-ups-1ie5</link>
      <guid>https://dev.to/douzatan/claude-code-android-automation-without-adb-tasks-states-and-follow-ups-1ie5</guid>
      <description>&lt;p&gt;Claude Code Android automation does not need adb, a USB cable or a local emulator if the phone lives in the cloud: you install a skill, add an access token, and Claude Code sends tasks in plain English to a real Android device, then polls them until they finish or need you. Setup is four commands and one token.&lt;/p&gt;

&lt;p&gt;The device in this tutorial comes from &lt;a href="https://airtap.ai/" rel="noopener noreferrer"&gt;Airtap&lt;/a&gt;, which runs a real Android phone for you in an isolated cloud container, and the bridge is its open-source skill at &lt;a href="https://github.com/airtap-ai/airtap-skill" rel="noopener noreferrer"&gt;github.com/airtap-ai/airtap-skill&lt;/a&gt;. Every command below comes from that repo's README and &lt;a href="https://github.com/airtap-ai/airtap-skill/blob/main/skills/airtap/SKILL.md" rel="noopener noreferrer"&gt;&lt;code&gt;skills/airtap/SKILL.md&lt;/code&gt;&lt;/a&gt;, checked against the main branch in September 2026. This is for developers who already use Claude Code or Codex and want it to operate apps on a remote phone. Building or testing your own APK is out of scope.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude plugin marketplace add airtap-ai/airtap-skill
claude plugin &lt;span class="nb"&gt;install &lt;/span&gt;airtap@airtap
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AIRTAP_PERSONAL_ACCESS_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;token from Settings in the Airtap app&amp;gt;"&lt;/span&gt;
python3 scripts/airtap.py task create &lt;span class="nt"&gt;--message&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;what to do&amp;gt;"&lt;/span&gt; &lt;span class="nt"&gt;--receiver-id&lt;/span&gt; cloud
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How does Claude Code Android automation work without ADB?
&lt;/h2&gt;

&lt;p&gt;Most Android tools for Claude Code are MCP servers that drive a phone plugged into your machine through &lt;code&gt;adb&lt;/code&gt;, so Claude Code decides every tap. This skill splits the work differently. Claude Code writes the task in plain language, and Airtap's own agent, working on the device, does the tapping.&lt;/p&gt;

&lt;p&gt;The path looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Claude Code picks up the &lt;code&gt;airtap&lt;/code&gt; skill and runs its bundled CLI, &lt;code&gt;scripts/airtap.py&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The CLI calls the Airtap API over HTTPS with your token as a Bearer header.&lt;/li&gt;
&lt;li&gt;The task goes to a receiver: &lt;code&gt;cloud&lt;/code&gt; (the hosted phone, and the default) or a physical Android phone you linked through the Autopilot app.&lt;/li&gt;
&lt;li&gt;The on-device agent works through the app screen by screen and writes progress messages back to the task.&lt;/li&gt;
&lt;li&gt;Claude Code polls those messages and decides what to tell you.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The trade-off is control. With adb you script every coordinate. Here you hand off one task per request and manage its lifecycle, which is most of what follows.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need before you start
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Claude Code or Codex. Cursor, Windsurf, Cline and GitHub Copilot also work through &lt;code&gt;npx skills&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;python3&lt;/code&gt; with &lt;code&gt;requests&lt;/code&gt; and &lt;code&gt;python-dotenv&lt;/code&gt; (the skill's whole &lt;code&gt;requirements.txt&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Node.js if you use the &lt;code&gt;npx skills&lt;/code&gt; route.&lt;/li&gt;
&lt;li&gt;An Airtap account. Sign in at airtap.ai/app with Google. No paid plan is needed; a per-account daily limit applies and resets at 00:00 UTC.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: install the skill
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Claude Code
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude plugin marketplace add airtap-ai/airtap-skill
claude plugin &lt;span class="nb"&gt;install &lt;/span&gt;airtap@airtap
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Per the &lt;a href="https://code.claude.com/docs/en/discover-plugins" rel="noopener noreferrer"&gt;Claude Code plugin docs&lt;/a&gt;, a plugin you install from the shell loads the next time you start Claude Code, or right away if you run &lt;code&gt;/reload-plugins&lt;/code&gt; in an open session. &lt;code&gt;claude plugin list&lt;/code&gt; confirms it's installed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Codex
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add airtap-ai/airtap-skill &lt;span class="nt"&gt;-a&lt;/span&gt; codex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cursor, Windsurf, Cline or GitHub Copilot
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;-a&lt;/code&gt; flag of &lt;a href="https://github.com/vercel-labs/skills" rel="noopener noreferrer"&gt;&lt;code&gt;npx skills&lt;/code&gt;&lt;/a&gt; picks the agent:&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 airtap-ai/airtap-skill &lt;span class="nt"&gt;-a&lt;/span&gt; cursor
npx skills add airtap-ai/airtap-skill &lt;span class="nt"&gt;-a&lt;/span&gt; windsurf
npx skills add airtap-ai/airtap-skill &lt;span class="nt"&gt;-a&lt;/span&gt; cline
npx skills add airtap-ai/airtap-skill &lt;span class="nt"&gt;-a&lt;/span&gt; github-copilot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="" class="article-body-image-wrapper"&gt;&lt;img alt="Screenshot of the airtap-ai/airtap-skill GitHub repository with the Claude Code install commands in the README"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The public repo; the install commands above come from this README.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If the dependencies are missing, run this from the skill directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip3 &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: add your personal access token
&lt;/h2&gt;

&lt;p&gt;In the Airtap web app, open Settings, create a personal access token and copy it. Then give it to the skill in one of two ways.&lt;/p&gt;

&lt;p&gt;Set an environment variable before you launch your agent:&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;export &lt;/span&gt;&lt;span class="nv"&gt;AIRTAP_PERSONAL_ACCESS_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-personal-access-token"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or write it into &lt;code&gt;scripts/.env&lt;/code&gt; from the skill directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 scripts/airtap.py &lt;span class="nt"&gt;--add-token&lt;/span&gt; &lt;span class="s2"&gt;"your-personal-access-token"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two repo details matter. Values in &lt;code&gt;scripts/.env&lt;/code&gt; override a shell variable of the same name, so a stale &lt;code&gt;.env&lt;/code&gt; beats a fresh &lt;code&gt;export&lt;/code&gt;. And SKILL.md tells the agent not to ask you to paste the token into chat, so run this yourself in a terminal.&lt;/p&gt;

&lt;p&gt;Skip this step and the first API call fails fast. This is what &lt;code&gt;receiver get-list&lt;/code&gt; prints with no token set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: Missing auth token. Run --add-token or add AIRTAP_PERSONAL_ACCESS_TOKEN to .env before running this command.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: run a first task on the cloud phone
&lt;/h2&gt;

&lt;p&gt;You don't type CLI commands yourself. Ask Claude Code something like "Use the Airtap skill to open the Play Store on the cloud phone and list the apps on the home screen," and the skill turns it into:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 scripts/airtap.py task create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--message&lt;/span&gt; &lt;span class="s2"&gt;"Open the Play Store and list the apps on the home screen"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--receiver-id&lt;/span&gt; cloud
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response includes a &lt;strong&gt;&lt;code&gt;taskId&lt;/code&gt;&lt;/strong&gt;, which every later command needs. Also note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The cloud phone takes about 60 seconds to set up the first time.&lt;/li&gt;
&lt;li&gt;Some apps come pre-installed. Sign in to Google Play on the phone for the rest.&lt;/li&gt;
&lt;li&gt;You log in to your own accounts yourself, once. The web dashboard shows the cloud phone's live screen.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To target your own linked phone instead, run &lt;code&gt;receiver get-list&lt;/code&gt; and pass its ID. SKILL.md tells agents to stay on &lt;code&gt;cloud&lt;/code&gt; unless you ask for a specific device.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: watch the task, answer it, or stop it
&lt;/h2&gt;

&lt;p&gt;A task can take a few seconds or several minutes, so the repo treats it as a long-running job. Monitor it with &lt;code&gt;task poll&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 scripts/airtap.py task poll &lt;span class="nt"&gt;--task-id&lt;/span&gt; &lt;span class="s2"&gt;"task_abc123"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It calls &lt;code&gt;task get-details&lt;/code&gt; every 10 seconds (change that with &lt;code&gt;--interval-secs&lt;/code&gt;). It stops by itself once the task completes, fails, is cancelled or needs input, and it returns the full task payload plus a &lt;code&gt;_poll&lt;/code&gt; summary. In that payload, &lt;code&gt;messages&lt;/code&gt; is the conversation in order, and most progress updates come from the newest &lt;code&gt;type: "agent"&lt;/code&gt; message.&lt;/p&gt;

&lt;p&gt;Send more input, or resume a paused task:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 scripts/airtap.py task add-user-message &lt;span class="nt"&gt;--task-id&lt;/span&gt; &lt;span class="s2"&gt;"task_abc123"&lt;/span&gt; &lt;span class="nt"&gt;--message&lt;/span&gt; &lt;span class="s2"&gt;"Continue"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stop it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 scripts/airtap.py task cancel &lt;span class="nt"&gt;--task-id&lt;/span&gt; &lt;span class="s2"&gt;"task_abc123"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you leave out &lt;code&gt;--model-id&lt;/code&gt;, tasks run on &lt;code&gt;airtap-1.0-flash&lt;/code&gt;, which is quicker and fine for short jobs. For long multi-step flows, pass &lt;code&gt;--model-id airtap-1.0&lt;/code&gt;. It's slower, but more reliable on complex work. Both &lt;code&gt;task create&lt;/code&gt; and &lt;code&gt;add-user-message&lt;/code&gt; also accept &lt;code&gt;--image-file&lt;/code&gt; if a screenshot or reference picture helps.&lt;/p&gt;

&lt;h2&gt;
  
  
  What do the task states mean for your agent?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;task poll&lt;/code&gt; exits on six states. &lt;strong&gt;Three are final, and three mean a human is needed.&lt;/strong&gt; This table maps each one to what SKILL.md tells the agent to do:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;taskState&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;What happened&lt;/th&gt;
&lt;th&gt;What your agent should do&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;COMPLETED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Task finished&lt;/td&gt;
&lt;td&gt;Summarize the latest agent messages&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FAILED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Task could not finish&lt;/td&gt;
&lt;td&gt;Report the state and the last agent message&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CANCELLED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Task was cancelled&lt;/td&gt;
&lt;td&gt;Report it&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WAITING_FOR_USER_INPUT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The agent needs a clarification&lt;/td&gt;
&lt;td&gt;Ask you, forward the answer&lt;/td&gt;
&lt;td&gt;&lt;code&gt;task add-user-message&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WAITING_FOR_USER_INTERVENTION&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Someone has to act on the device (a login, for example)&lt;/td&gt;
&lt;td&gt;Tell you exactly what to do, wait for your OK, resume&lt;/td&gt;
&lt;td&gt;&lt;code&gt;task add-user-message&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WAITING_FOR_USER_CONTINUE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Task hit its step limit&lt;/td&gt;
&lt;td&gt;Summarize progress, ask whether to continue&lt;/td&gt;
&lt;td&gt;&lt;code&gt;task add-user-message&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Claude Code already reads these rules from SKILL.md. To pin the same behavior for every agent in a repo, add this to &lt;code&gt;CLAUDE.md&lt;/code&gt; or &lt;code&gt;AGENTS.md&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Android tasks (Airtap skill)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Use receiver &lt;span class="sb"&gt;`cloud`&lt;/span&gt; unless I name a device.
&lt;span class="p"&gt;-&lt;/span&gt; After &lt;span class="sb"&gt;`task create`&lt;/span&gt;, run &lt;span class="sb"&gt;`task poll`&lt;/span&gt; and wait. Do not create a second task for the same request.
&lt;span class="p"&gt;-&lt;/span&gt; On any WAITING_&lt;span class="err"&gt;*&lt;/span&gt; state, stop and ask me. Never answer on my behalf.
&lt;span class="p"&gt;-&lt;/span&gt; Never ask me to paste the access token into chat.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How should you word Android tasks for an agent?
&lt;/h2&gt;

&lt;p&gt;The on-device agent reads the screen at each step, so a vague task still runs, but it wanders. Habits that help:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Name the app and the finish line. "In the Weather app, read tomorrow's high for Chicago and stop" is better than "check the weather".&lt;/li&gt;
&lt;li&gt;Say what must not happen: "do not place the order", "do not send the message".&lt;/li&gt;
&lt;li&gt;Put the output format in the task so the summary is easy to parse.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three task messages written that way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Open the Play Store, search for "Google Keep", and report the rating and last-updated date of the official app. Do not install anything.
In Google Maps, find the three closest pharmacies to Union Square, San Francisco that are open now. Return name and closing time.
Open the Gmail app, count unread emails from the last 24 hours, and list the five senders. Do not open or archive any email.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a task depends on where you are, update your location first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 scripts/airtap.py user update-location &lt;span class="nt"&gt;--payload&lt;/span&gt; &lt;span class="s1"&gt;'{"latitude":37.7880,"longitude":-122.4075}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Troubleshooting the Airtap skill
&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;Likely cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Missing auth token...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No token in the environment or &lt;code&gt;scripts/.env&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Step 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The new token is ignored&lt;/td&gt;
&lt;td&gt;An old &lt;code&gt;scripts/.env&lt;/code&gt; overrides the shell variable&lt;/td&gt;
&lt;td&gt;Edit or delete &lt;code&gt;scripts/.env&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Airtap API request failed with status 404&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SKILL.md: on any 404 or request failure, get an updated skill before retrying&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;claude plugin marketplace update airtap&lt;/code&gt;, or &lt;code&gt;npx skills update airtap&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code doesn't list the skill&lt;/td&gt;
&lt;td&gt;Plugin not loaded yet&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/reload-plugins&lt;/code&gt;, then check &lt;code&gt;claude plugin list&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Skill missing on claude.ai/code&lt;/td&gt;
&lt;td&gt;Cloud sessions don't load plugins you installed locally&lt;/td&gt;
&lt;td&gt;Run Claude Code locally&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ModuleNotFoundError: No module named 'requests'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Dependencies missing&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pip3 install -r requirements.txt&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;python3&lt;/code&gt; not found on Windows&lt;/td&gt;
&lt;td&gt;The interpreter is called &lt;code&gt;python&lt;/code&gt; or &lt;code&gt;py&lt;/code&gt; there&lt;/td&gt;
&lt;td&gt;Swap the command name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Task stuck in a WAITING state&lt;/td&gt;
&lt;td&gt;It's waiting on you&lt;/td&gt;
&lt;td&gt;See the state table&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When a request fails without a clear message, check the CLI's debug log at &lt;code&gt;airtap/airtap.log&lt;/code&gt; in your system temp directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits and security notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;This is not a test harness. You get task results and progress messages, not per-tap assertions. For testing your own app, use adb-based tools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logins stay with you.&lt;/strong&gt; According to Airtap's homepage, passwords are never stored or shown to the agent, and the agent is blocked from reading secure fields. That also means a task can pause in &lt;code&gt;WAITING_FOR_USER_INTERVENTION&lt;/code&gt; until you type something yourself.&lt;/li&gt;
&lt;li&gt;Live screen view is only for the cloud phone. Physical devices linked through Autopilot have no live view yet, so poll output is all you see there. Autopilot is Android only.&lt;/li&gt;
&lt;li&gt;Treat the token like any API key. Keep &lt;code&gt;scripts/.env&lt;/code&gt; out of git and out of anything you screenshot.&lt;/li&gt;
&lt;li&gt;A loop that keeps re-creating failed tasks eats the daily soft cap. Limit retries.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quick answers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can Claude Code drive an iPhone this way?&lt;/strong&gt; No. The receivers are Android: the cloud phone or your own Android phone through Autopilot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can it use my real phone instead of the cloud one?&lt;/strong&gt; Yes. Link it with the Autopilot Android app, find its ID with &lt;code&gt;receiver get-list&lt;/code&gt;, and ask for that device by name.&lt;/p&gt;

&lt;p&gt;Once your first &lt;code&gt;task poll&lt;/code&gt; comes back &lt;code&gt;COMPLETED&lt;/code&gt;, the setup is done. What's left is wording, and the three task messages above are a reasonable template for the next one.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>android</category>
      <category>automation</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Treat Your Product Line Art Like Code: A Docs-as-Code Pipeline from Photo to Spec-Sheet Flat</title>
      <dc:creator>douzatan</dc:creator>
      <pubDate>Thu, 17 Sep 2026 16:25:06 +0000</pubDate>
      <link>https://dev.to/douzatan/treat-your-product-line-art-like-code-a-docs-as-code-pipeline-from-photo-to-spec-sheet-flat-3120</link>
      <guid>https://dev.to/douzatan/treat-your-product-line-art-like-code-a-docs-as-code-pipeline-from-photo-to-spec-sheet-flat-3120</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fflu6m8qvi5vgk7ixq0va.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fflu6m8qvi5vgk7ixq0va.webp" alt="The AI Line Art Generator tool page on Virse, showing the Detailed line art, Clean line art, and Bold outline buttons above an upload panel and an input and output preview" width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The AI Line Art Generator tool page on Virse, the generation step this pipeline builds around&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A product photo to line drawing conversion is the easy part of hardware documentation. Keeping that drawing correct through the next five product revisions, without anyone quietly re-uploading a photo by hand every time the part changes, is the part most documentation pipelines get wrong.&lt;/p&gt;

&lt;p&gt;Most teams writing hardware docs already treat their Markdown, their API references, and their release notes as code: version controlled, reviewed in pull requests, rebuilt automatically the moment something upstream changes. Then a designer generates one line drawing for a spec sheet, drops the PNG into a shared drive, and that file quietly becomes the one artifact in the whole documentation set nobody can trace back to a source photo, a parameter, or a revision. Screenshots and diagrams are already the part of a doc set that goes stale first, mostly because updating an image takes one extra manual step that updating a sentence does not. Line art generated from a product photo has the same problem, and it is worth solving the same way you would solve any other pipeline gap: give the step a predictable input, a predictable output, a stated reason to rerun it, and a way for a teammate to review what changed.&lt;/p&gt;
&lt;h2&gt;
  
  
  Product photo to line drawing: a one-time conversion versus a pipeline step
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5iuek6q4hel2cjbmc4w9.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5iuek6q4hel2cjbmc4w9.webp" alt="A cordless drill product photo next to its traced line drawing, showing that the tool reads a real photo instead of a text prompt" width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The drawing is traced from a real photo, which is exactly why it cannot be regenerated from a text prompt alone&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A tool that turns a photo into line art is reading the shape of a real object and tracing it, not guessing at a shape from a text prompt. That single fact is why this kind of asset cannot be treated the same way you treat a diagram written in Mermaid or PlantUML syntax. A diagram-as-code file is source code. Check it into Git, and anyone can regenerate the exact same picture from the exact same text forever. A line drawing pulled from a photo has no text equivalent to check in. The only way to regenerate it is to keep the photo, the line style, and any notes about what to keep or drop, and run the generation step again with those same three inputs.&lt;/p&gt;

&lt;p&gt;Treat the photo, the parameters, and the result as one unit that travels together, and a "one-time conversion" turns into an ordinary pipeline step with a beginning and an end you can point to. Skip that step, and the drawing becomes exactly the kind of orphaned asset a docs-as-code setup was supposed to prevent in the first place.&lt;/p&gt;
&lt;h2&gt;
  
  
  What a docs-as-code pipeline expects from any generated asset
&lt;/h2&gt;

&lt;p&gt;A typical docs-as-code setup gets built around plain text formats, Git as the single source of truth, continuous integration that rebuilds the site automatically, pull request review, and a linter that catches formatting drift. That checklist is written for prose, and it usually has exactly one line about images: automate your screenshots. A generated line drawing needs the same four things any other pipeline output needs, and none of the four are specific to line art at all:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A predictable input: the source photo, plus whatever settings actually produced this exact result&lt;/li&gt;
&lt;li&gt;A predictable output location: a path that does not depend on someone remembering where they saved a file three weeks ago&lt;/li&gt;
&lt;li&gt;A rerun trigger: a stated, specific reason the step needs to run again, not a vague sense that the drawing looks old&lt;/li&gt;
&lt;li&gt;A reviewable change: some way for a teammate to look at a pull request and understand what actually moved between the old drawing and the new one&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rest of this piece works through what those four things look like once you make them concrete for a product line drawing instead of leaving them as an abstract checklist.&lt;/p&gt;
&lt;h2&gt;
  
  
  Naming the output: encode product, angle, line style, and revision in one filename
&lt;/h2&gt;

&lt;p&gt;A filename that only says &lt;code&gt;kettle-lineart-final.png&lt;/code&gt; fails the moment there is a second kettle revision, a second camera angle, or a second line style for the same product. A name that survives more than one revision needs to encode four things at once instead of one:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Segment&lt;/th&gt;
&lt;th&gt;Example value&lt;/th&gt;
&lt;th&gt;Why it needs to be there&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Product identifier&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kettle-900ml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Distinguishes this part from every other part living in the same repository&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Angle or view&lt;/td&gt;
&lt;td&gt;&lt;code&gt;front34&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A three-quarter front photo and a straight side photo of the same product are not interchangeable inputs, and a filename should say which one produced this drawing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Line style&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;detailed&lt;/code&gt;, &lt;code&gt;clean&lt;/code&gt;, or &lt;code&gt;bold&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The three real options on the generator, Detailed line art, Clean line art, and Bold outline, produce results that are not substitutes for each other&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Revision&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rev-b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Marks which iteration of the product this drawing was traced from&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Engineering drawings solved the revision part of this problem long before anyone generated a line drawing from a photo. Design-phase revisions use letters, construction or as-built phases switch over to numbers starting at 0, and the letters I, L, and O get skipped entirely because they are too easy to misread as the digits 1 and 0, especially on a faded printout. Borrowing that convention for generated line art costs nothing and buys the same benefit a factory floor has relied on for decades: &lt;code&gt;kettle-900ml-front34-detailed-rev-b.png&lt;/code&gt; tells a teammate everything they need to know about a file without opening it first.&lt;/p&gt;

&lt;p&gt;Generating the actual drawing is the one step in this pipeline a documentation team does not need to build in house. Upload a product photo with the background already removed, pick one of the three real options on the form, and download the black-on-white result. Virse's AI Line Art Generator works this way, and you can try it here to compare Detailed line art, Clean line art, and Bold outline on a real product photo before deciding which one becomes the canonical drawing for a given revision. What matters for the pipeline is not which tool draws the line. What matters is that whatever tool produces it, the result traces back to a real photo you can point to, with a line style you can name, instead of a result nobody can reproduce next quarter.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where the drawing lives: a folder a documentation build can actually trust
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fenge58gyvipegsw55er8.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fenge58gyvipegsw55er8.webp" alt="A printed flat sketch line drawing of a jacket used as a tech pack reference for manufacturing" width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A flat sketch like this is exactly the kind of asset a documentation build needs to trust the revision of&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The filename convention above only helps once the file actually lands somewhere predictable. A docs repository can hold generated line art the same way it holds any other build artifact, as long as the source photo and the result sit next to each other instead of the source living in someone's downloads folder while the result lives alone in the docs repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;assets/line-art/
  kettle-900ml/
    rev-b/
      source-front34.jpg
      params.json
      kettle-900ml-front34-detailed-rev-b.png
      kettle-900ml-front34-clean-rev-b.png
      kettle-900ml-front34-bold-rev-b.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;params.json&lt;/code&gt; is a small text file that records what actually happened when this drawing was made: which photo, which line style, which notes about seams or logos to keep or drop. That one file is what turns a folder of PNGs into something a documentation build can trust, because it answers "what would it take to regenerate this" without anyone having to track down whoever ran the original generation.&lt;/p&gt;

&lt;p&gt;This structure earns its keep exactly where a tech pack or a spec sheet needs it most. A flat sketch has to show a part's proportions, seams, and hardware accurately enough that a factory can build from it without a phone call back to the design team, and industry guidance on tech packs is blunt about what happens once a product changes: the technical design update has to reach the manufacturer without any ambiguity. A line drawing sitting in a folder with no record of which revision it traces back to cannot make that promise, no matter how clean the drawing itself looks.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to rerun the step: three triggers that actually justify a new drawing
&lt;/h2&gt;

&lt;p&gt;"This drawing looks a little old" is not a rerun trigger. A real trigger is specific enough to write into a pull request description on its own, and there are three that actually apply to a photo-derived line drawing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The product's geometry changed.&lt;/strong&gt; A new revision changes a dimension, adds a feature, or removes one. The old drawing is now factually wrong, not merely out of date.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The reference photo changed.&lt;/strong&gt; A better angle, a cleaner background removal, or a corrected framing produces a more faithful trace, even when the product itself did not change at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The downstream use changed the line style requirement.&lt;/strong&gt; A drawing that started life as a Clean line art base for a canvas re-render now has to go into a factory-facing spec sheet, which calls for Detailed line art instead.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Any one of those three is worth a new revision folder and a fresh &lt;code&gt;params.json&lt;/code&gt;. None of them is "someone thought the folder looked cluttered" or "it has been a while since anyone touched this."&lt;/p&gt;

&lt;h2&gt;
  
  
  Reviewing a line art change without a text diff
&lt;/h2&gt;

&lt;p&gt;A Markdown file diffs cleanly. Git shows exactly which line changed, and a reviewer can read the whole story in the pull request without opening anything else. A PNG does not diff that way, and pretending it does by staring at two images side by side and hoping to spot the difference does not scale past the first handful of reviews. The workaround is to stop trying to diff the pixels directly and start diffing the thing that actually explains them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fih8311a8ip3hff9ejeku.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fih8311a8ip3hff9ejeku.webp" alt="Schematic diagram of a docs-as-code line art pipeline with five nodes: photo, generate, name and version, merge into docs repo, and rerun on iteration, looping back to the start" width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Schematic: the rerun step loops back to the photo, it does not start a new pipeline&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Splitting the review into two separate questions handles this in practice. First: does &lt;code&gt;params.json&lt;/code&gt; show a change that matches the stated rerun trigger, a different source photo path, a different line style, a bumped revision letter? That part is an ordinary text diff, and any reviewer can read it inside a pull request without opening an image viewer at all. Second: does the new PNG, placed next to the old one, still look like the same object drawn to the same rules? That part still needs a human eye, but it becomes a much smaller and much faster check once the metadata diff has already confirmed the change was intentional and matches what the pull request claims it is doing.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What to check&lt;/th&gt;
&lt;th&gt;How to check it&lt;/th&gt;
&lt;th&gt;Automatable?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Did the right thing change&lt;/td&gt;
&lt;td&gt;Text diff of &lt;code&gt;params.json&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Does the new revision label match the claim in the PR&lt;/td&gt;
&lt;td&gt;Text diff of the folder path&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Does the new drawing still look like the actual product&lt;/td&gt;
&lt;td&gt;Side-by-side visual comparison&lt;/td&gt;
&lt;td&gt;No, this still needs a human&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What this pipeline still cannot automate away
&lt;/h2&gt;

&lt;p&gt;None of the structure above replaces judgment about the drawing itself. A tool that traces a real photo will not invent detail that was never in the picture, and it will not quietly drop the object's actual proportions just to produce a cleaner-looking line, but it also has no opinion about whether a given change deserves a new revision folder or whether the reference photo was clean enough to trust in the first place. That call belongs to whoever is building the documentation, the same way deciding to merge a pull request belongs to whoever is reviewing it, not to whatever tool generated the diff underneath.&lt;/p&gt;

&lt;p&gt;Treating a generated line drawing like code does not mean pretending it is text. It means giving it the same four things any other build artifact already gets in a docs-as-code setup: a name that says what it is, a folder that says where it came from, a stated reason it exists in this particular form, and a way for a teammate to check the change without taking a screenshot on faith. Once that structure is in place, regenerating a drawing after the sixth product revision is a rerun of a known step, not an afternoon spent trying to remember which photo the first version ever came from.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>YouTube transcript to documentation: a reviewable workflow</title>
      <dc:creator>douzatan</dc:creator>
      <pubDate>Tue, 15 Sep 2026 16:15:02 +0000</pubDate>
      <link>https://dev.to/douzatan/youtube-transcript-to-documentation-a-reviewable-workflow-2a0p</link>
      <guid>https://dev.to/douzatan/youtube-transcript-to-documentation-a-reviewable-workflow-2a0p</guid>
      <description>&lt;p&gt;Turning a YouTube transcript to documentation means organizing the spoken explanation around a task, checking the technical details, and keeping links to the moments that support them. The deliverable should let another developer complete the task and review the instructions without replaying the entire video.&lt;/p&gt;

&lt;p&gt;This AllyHub guide uses &lt;a href="https://allyhub.com/tools/youtube-transcript-extractor" rel="noopener noreferrer"&gt;AllyHub's transcript extractor&lt;/a&gt; to obtain published captions with timestamps. Extraction supplies the source material; a documentation author still needs to verify commands, fill in prerequisites, and decide which explanation belongs in the final page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Working order:&lt;/strong&gt; collect the transcript, map the source, write the task, check the result.&lt;/p&gt;

&lt;p&gt;In this guide: source map, technical checks, task draft, review.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you turn a YouTube transcript to documentation?
&lt;/h2&gt;

&lt;p&gt;Start with a video that covers one identifiable task, then use its timestamped transcript to build a task-based outline. For a first pass, choose something bounded, such as configuring a local project or explaining one deployment setting.&lt;/p&gt;

&lt;p&gt;YouTube's own transcript view can help you inspect the source. For videos with captions, the transcript lets you jump from a line of text to the corresponding moment in the video, according to &lt;a href="https://support.google.com/youtube/answer/15930243?hl=en" rel="noopener noreferrer"&gt;YouTube Help&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That jump matters when a sentence depends on a screen the reader cannot see. A speaker might say "change this value" while highlighting a configuration key. The transcript preserves the words, but the documentation needs the key, its intended value, and the reason for the change.&lt;/p&gt;

&lt;p&gt;Before drafting, write down the intended reader and the starting state. "A teammate with the repository cloned but no local configuration" is a useful boundary. It tells you which setup steps the page must include.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep a source map before editing
&lt;/h2&gt;

&lt;p&gt;Make a small table that connects each planned section to its supporting passage. This is a working record for the author and reviewer, so include uncertainty while it is still easy to resolve.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Documentation section&lt;/th&gt;
&lt;th&gt;Source to keep&lt;/th&gt;
&lt;th&gt;What to verify&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Prerequisites&lt;/td&gt;
&lt;td&gt;Video URL and relevant timestamp&lt;/td&gt;
&lt;td&gt;Versions, permissions, and required files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Main procedure&lt;/td&gt;
&lt;td&gt;Passage and the screen shown at that moment&lt;/td&gt;
&lt;td&gt;Exact commands and order of operations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expected result&lt;/td&gt;
&lt;td&gt;Demonstration of a successful result&lt;/td&gt;
&lt;td&gt;What success looks like for the current setup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Troubleshooting&lt;/td&gt;
&lt;td&gt;Error discussed or demonstrated&lt;/td&gt;
&lt;td&gt;Whether the remedy applies to the documented version&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Keep the raw transcript alongside your edited notes. If an automatic caption misspells a package name, retain the original in the source file and correct it in the draft after checking the video or the package documentation.&lt;/p&gt;

&lt;p&gt;Use the source map to find missing steps. If the video jumps from installation to a working application, the author must find out what happened between those states before calling the page a complete setup guide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which parts of a tutorial need independent checking?
&lt;/h2&gt;

&lt;p&gt;Check every instruction whose correctness depends on exact syntax, software version, or the reader's environment. Captions can help locate those details, but they should not be treated as a command reference.&lt;/p&gt;

&lt;p&gt;Pay particular attention to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Commands that create, change, or remove files.&lt;/li&gt;
&lt;li&gt;Environment variable names and configuration keys.&lt;/li&gt;
&lt;li&gt;Prerequisites that the presenter already had installed.&lt;/li&gt;
&lt;li&gt;Settings shown on screen but never spoken aloud.&lt;/li&gt;
&lt;li&gt;Error messages and the conditions that produced them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use the official documentation for the actual version you are describing. If the video covers an older release, either scope the article to that release or update and verify the affected instructions.&lt;/p&gt;

&lt;p&gt;Do not silently repair a command because it looks plausible. Mark it as unverified in your working notes, find the relevant source, and test it in a suitable environment before including it in an executable tutorial. The published page should contain the resolved instruction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn the transcript into a task someone can finish
&lt;/h2&gt;

&lt;p&gt;Organize the draft around what the reader must do. A video may include a long introduction, a failed attempt, and an explanation that only arrives after the demonstration. Written documentation can put the prerequisite first and move the failure into troubleshooting.&lt;/p&gt;

&lt;p&gt;For each step, supply an action and an observable result. "Configure the project" leaves too much open. "Create the local configuration file, add the documented variables, then start the development server" gives the reader a sequence, though the final page still needs the exact names and commands.&lt;/p&gt;

&lt;p&gt;Use this copyable structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Task:
Starting state:
Required versions and access:
Source video and timestamps:

Steps:
For each step, record the action and expected result.

Verification:
Explain how the reader can confirm the task succeeded.

Troubleshooting:
Name the symptom, likely cause, and checked remedy.

Last technical check:
Record the environment and the date of verification.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Review the draft against the source
&lt;/h2&gt;

&lt;p&gt;Review source accuracy and task completion separately. A passage can accurately summarize what the speaker said while leaving the reader unable to reproduce the result.&lt;/p&gt;

&lt;p&gt;First, compare the draft with the video at the saved timestamps. Check names, quoted statements, and the meaning of each explanation. Then follow the instructions from the declared starting state, recording any missing step or unexpected result.&lt;/p&gt;

&lt;p&gt;The final handoff should contain the finished page and a compact source record. Keep draft uncertainties out of the published instructions, but preserve the evidence that resolved them in your working files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions that come up during the handoff
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can you do this when a video has no captions?
&lt;/h3&gt;

&lt;p&gt;The caption-based workflow needs an available caption track. If none exists, obtain a transcript through a separate transcription process or work from the recording manually. Check the resulting text against the audio and screen before using it as a technical source.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should every sentence have a timestamp?
&lt;/h3&gt;

&lt;p&gt;Usually the working source map can hold the detailed timestamps. In the published page, retain direct links for quotations, demonstrations, or explanations a reader may need to inspect. The goal is a reviewable document that remains comfortable to read.&lt;/p&gt;

&lt;p&gt;For your next YouTube transcript to documentation task, start with one procedure and one reviewer. Finish when that reviewer can follow the page and trace any uncertain instruction back to its source.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Onboarding-Videos aus Team-Dokumentation generieren: unser Workflow nach drei Monaten</title>
      <dc:creator>douzatan</dc:creator>
      <pubDate>Sat, 08 Aug 2026 15:45:09 +0000</pubDate>
      <link>https://dev.to/douzatan/onboarding-videos-aus-team-dokumentation-generieren-unser-workflow-nach-drei-monaten-3f6m</link>
      <guid>https://dev.to/douzatan/onboarding-videos-aus-team-dokumentation-generieren-unser-workflow-nach-drei-monaten-3f6m</guid>
      <description>&lt;p&gt;Unser Team hat ein klassisches Problem gelöst, das vermutlich jedes wachsende Entwicklerteam kennt: Die Onboarding-Doku liest niemand, und für Videos hat niemand Zeit.&lt;/p&gt;

&lt;p&gt;Die Lösung war unspektakulär pragmatisch. Wir generieren die Videos jetzt direkt aus den Dokumenten, die wir sowieso pflegen. Kein Kamera-Setup, kein Schnittprogramm, keine zusätzliche Content-Pipeline.&lt;/p&gt;

&lt;p&gt;Dieser Post beschreibt den Workflow, die Einstellungen, die tatsächlich einen Unterschied machen, und die Stellen, an denen der Ansatz an seine Grenzen stößt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ausgangslage: 40 Seiten Confluence, die keiner liest
&lt;/h2&gt;

&lt;p&gt;Wir sind ein Team von elf Leuten. Unsere Onboarding-Doku war solide gepflegt: Architektur-Überblick, Deployment-Prozess, Coding-Standards, Incident-Runbooks.&lt;/p&gt;

&lt;p&gt;Das Problem war nicht die Qualität. Das Problem war das Format. Neue Kollegen bekamen in der ersten Woche einen Berg Text, und die immer gleichen Fragen landeten trotzdem im Team-Channel.&lt;/p&gt;

&lt;p&gt;Der naheliegende Gedanke "machen wir doch Videos draus" scheiterte zweimal an der Realität: Wer selbst aufnimmt, produziert bei unserem Doku-Umfang wochenlang. Und bei jedem Release veraltet ein Teil der Aufnahmen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Der Kerngedanke: Docs bleiben die Source of Truth
&lt;/h2&gt;

&lt;p&gt;Der Ansatz, der bei uns funktioniert, folgt derselben Logik wie Docs-as-Code: Es gibt genau eine Quelle, und alles andere ist ein generiertes Artefakt.&lt;/p&gt;

&lt;p&gt;Das Dokument bleibt das Original. Das Video wird daraus erzeugt. Ändert sich der Inhalt, wird das Dokument editiert und das Video neu generiert.&lt;/p&gt;

&lt;p&gt;Damit verschwindet das Problem, das jede Video-Initiative in Teams killt: der Pflegeaufwand. Ein veraltetes Video ist bei uns kein Neuaufnahme-Projekt mehr, sondern ein Regenerieren mit aktualisierter Quelle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Der Workflow im Detail
&lt;/h2&gt;

&lt;p&gt;Wir nutzen dafür den AI-Learning-Video-Generator von Leadde. Der Ablauf besteht aus vier Schritten.&lt;/p&gt;

&lt;h3&gt;
  
  
  Schritt 1: Dokument hochladen
&lt;/h3&gt;

&lt;p&gt;Unterstützt werden .pptx, .pdf, .doc, .docx und .txt bis 500 MB. Wir exportieren die jeweilige Doku-Seite als PDF oder Word-Datei.&lt;/p&gt;

&lt;p&gt;Ein Detail, das uns Arbeit erspart hat: Diagramme und Screenshots aus dem Quelldokument werden geprüft und, wenn brauchbar, direkt ins Video übernommen. Unsere Architektur-Diagramme mussten wir nicht neu bauen.&lt;/p&gt;

&lt;p&gt;Alternativ akzeptiert das Tool auch reinen Text oder einen Prompt, aus dem es selbst eine Struktur baut. Für Doku-Fragmente ohne saubere Datei ist das der schnellere Weg.&lt;/p&gt;

&lt;h3&gt;
  
  
  Schritt 2: Output konfigurieren
&lt;/h3&gt;

&lt;p&gt;Die Einstellungen, die bei uns den Unterschied machen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Avatar: über 200 zur Auswahl. Wir haben einen festgelegt und verwenden ihn für die gesamte Serie. Konsistenz schlägt Abwechslung.&lt;/li&gt;
&lt;li&gt;Länge: vier Presets. Zusammenfassung (1 bis 3 Minuten), ausgewogen (3 bis 5), vertieft (5 bis 7), umfassend je nach Quellumfang. Für Onboarding-Module nehmen wir fast immer das ausgewogene Preset.&lt;/li&gt;
&lt;li&gt;Sprache: 88 Optionen. Wir generieren auf Deutsch und für zwei englischsprachige Kollegen zusätzlich auf Englisch, aus derselben Quelle.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Der unterschätzte Teil sind die Kontextfelder unter "Weitere Einstellungen": Erzählstil, Zielgruppe, Sprecherhintergrund, Lernziel. Ein Skript, das mit "Zielgruppe: Backend-Entwickler, neu im Team, kennt unseren Stack nicht" generiert wurde, unterscheidet sich deutlich von einem ohne Kontext. Diese Felder leer zu lassen ist der häufigste Anfängerfehler.&lt;/p&gt;

&lt;h3&gt;
  
  
  Schritt 3: Skript reviewen, wie ein Pull Request
&lt;/h3&gt;

&lt;p&gt;Nach der Analyse liefert das Tool eine Gliederung plus komplettes Sprecherskript. Dann gibt es zwei Buttons: im Studio editieren oder direkt generieren.&lt;/p&gt;

&lt;p&gt;Wir behandeln das Skript wie einen PR: Es wird nie ungelesen gemerged. Der Review dauert bei uns zehn Minuten pro Video und folgt einer festen Checkliste:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Einstieg umschreiben. Die generierten Intros sind korrekt, aber generisch.&lt;/li&gt;
&lt;li&gt;Fachbegriffe prüfen. Interne Projektnamen und Abkürzungen paraphrasiert die KI gern falsch.&lt;/li&gt;
&lt;li&gt;Aussprache fixen. Dafür gibt es ein eigenes Tool; einmal korrigiert, sitzt der Begriff in allen folgenden Videos.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Für Serien-Konsistenz nutzen wir zusätzlich die Knowledge-Base-Funktion: Referenzmaterial hinterlegen, damit die Terminologie über alle Videos stabil bleibt statt zu driften.&lt;/p&gt;

&lt;h3&gt;
  
  
  Schritt 4: Generieren und Stichprobe
&lt;/h3&gt;

&lt;p&gt;Die Generierung läuft ein paar Minuten pro Video. Wir queuen mehrere und reviewen im Block.&lt;/p&gt;

&lt;p&gt;Der finale Check: Untertitel kontrollieren (automatisch generiert, Styling anpassbar), Szenenübergänge prüfen, einmal komplett in 1,5-facher Geschwindigkeit ansehen. Einzelne Szenen lassen sich isoliert korrigieren, ohne den Rest anzufassen. Das macht Wartung realistisch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zahlen nach drei Monaten
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;22 Onboarding-Module als Video, generiert aus bestehender Doku. Reale Arbeitszeit: rund 8 Stunden insgesamt.&lt;/li&gt;
&lt;li&gt;Aufwand pro Video: 15 bis 25 Minuten, der Großteil davon Skript-Review.&lt;/li&gt;
&lt;li&gt;Zwei Releases mit Doku-Änderungen seither. Update-Aufwand: Dokument editieren, regenerieren, fertig. Früher wäre das der Punkt gewesen, an dem die Videos verwaist wären.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Der messbare Effekt im Team: Die Wiederholungsfragen im Channel sind spürbar zurückgegangen, und neue Kollegen kommen mit konkreteren Fragen in die erste Pairing-Session. Kein kontrolliertes Experiment, klar. Aber der Trend war deutlich genug, dass wir den Rest der Doku nachziehen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wo der Ansatz an Grenzen stößt
&lt;/h2&gt;

&lt;p&gt;Drei ehrliche Einschränkungen.&lt;/p&gt;

&lt;p&gt;Erstens: Das Skript braucht immer einen menschlichen Review. Die Struktur stimmt, der Ton ist austauschbar. Wer ungeprüft published, bekommt eine Videobibliothek, die klingt wie von einer Maschine, weil sie es ist.&lt;/p&gt;

&lt;p&gt;Zweitens: Ein Avatar ersetzt keinen guten Live-Workshop. Für interaktive Formate, Pairing und Architektur-Diskussionen bleibt der Mensch gesetzt. Die generierten Videos decken den reproduzierbaren Teil ab, nicht den dialogischen.&lt;/p&gt;

&lt;p&gt;Drittens: Garbage in, garbage out. Schlecht strukturierte Doku ergibt schlecht strukturierte Videos. Der Ansatz belohnt Teams, die ihre Doku ohnehin ernst nehmen, und bestraft die anderen doppelt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integration in den bestehenden Doku-Prozess
&lt;/h2&gt;

&lt;p&gt;Der Workflow oben beschreibt das Generieren. Interessanter ist, wie das Ganze dauerhaft im Team-Prozess lebt, ohne dass es eine Person "nebenbei" tragen muss.&lt;/p&gt;

&lt;p&gt;Unsere Regeln nach drei Monaten Iteration:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ownership folgt der Doku.&lt;/strong&gt; Wer ein Doku-Kapitel pflegt, pflegt auch dessen Video. Kein separates Video-Team, keine Übergabe. Das Video ist ein Artefakt des Kapitels, wie ein generiertes Diagramm.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Regenerieren ist Teil der Definition of Done.&lt;/strong&gt; Ändert ein PR die Onboarding-Doku substanziell, gehört das Neu-Generieren des betroffenen Videos zur Fertigstellung. Klingt streng, kostet real aber die erwähnten 15 bis 25 Minuten und verhindert das schleichende Veralten, an dem unsere früheren Video-Anläufe gestorben sind.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ein Namensschema von Anfang an.&lt;/strong&gt; Videos heißen bei uns wie die Doku-Kapitel, aus denen sie stammen, plus Versionsdatum. Banal, aber die Zuordnung Quelle-zu-Video muss ohne Nachdenken funktionieren, sonst regeneriert irgendwann jemand aus der falschen Version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quartalsweise Stichprobe.&lt;/strong&gt; Einmal pro Quartal sieht sich ein Teammitglied drei zufällige Videos komplett an. Findet es veraltete Inhalte, war der Prozess davor undicht. Bisher zweimal passiert, beide Male ein fehlendes Regenerieren nach Doku-Änderung.&lt;/p&gt;

&lt;p&gt;Was wir bewusst nicht tun: Videos für Inhalte generieren, die sich wöchentlich ändern. Sprint-spezifisches bleibt Text. Die Video-Schicht lohnt sich ab einer Halbwertszeit von ungefähr einem Quartal, darunter frisst die Pflege den Nutzen.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ aus dem Team-Channel
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Braucht man Video-Editing-Kenntnisse?&lt;/strong&gt;&lt;br&gt;
Nein. Der Editor funktioniert wie ein Folien-Tool, nicht wie ein Schnittprogramm. Hochladen, konfigurieren, Skript reviewen, generieren. Wer Slides bauen kann, kann das hier auch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wie gut ist das deutsche Voiceover?&lt;/strong&gt;&lt;br&gt;
Besser als erwartet, mit einer Einschränkung: Fachbegriffe und Projektnamen brauchen gelegentlich einen manuellen Aussprache-Fix. Der ist einmalig pro Begriff und gilt danach für alle Videos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Was kostet ein Video an Zeit?&lt;/strong&gt;&lt;br&gt;
Nach der Einrichtungsphase 15 bis 25 Minuten Eigenaufwand. Die ersten zwei, drei Videos dauern länger, weil man seine Einstellungen noch sucht. Danach ist es Routine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lohnt sich das für kleine Teams?&lt;/strong&gt;&lt;br&gt;
Gerade für kleine. Große Organisationen haben L&amp;amp;D-Abteilungen; kleine Teams haben niemanden, dessen Job das ist. Genau diese Lücke füllt der generierte Ansatz.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Was ist mit vertraulichen Inhalten in der Doku?&lt;/strong&gt;&lt;br&gt;
Wir trennen vorher: Onboarding-Inhalte, die ins Video dürfen, liegen in eigenen Kapiteln, getrennt von Secrets, Kundendaten und internen Details. Diese Trennung hatten wir aus Doku-Hygiene ohnehin, sie zahlt sich hier doppelt aus. Zusätzlich lassen sich veröffentlichte Videos mit Passwortschutz versehen, was wir für teaminterne Module standardmäßig tun.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Warum nicht einfach Loom-Aufnahmen?&lt;/strong&gt;&lt;br&gt;
Haben wir davor gemacht, und die Aufnahmen waren nach zwei Releases veraltet und wurden nie aktualisiert, weil Neuaufnehmen Aufwand ist. Screencasts sind super für einmalige Erklärungen an eine Person. Für wiederverwendbare Module mit Update-Bedarf verliert das Format gegen generierte Videos, deren Quelle ein editierbares Dokument ist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fazit und Einstiegspunkt
&lt;/h2&gt;

&lt;p&gt;Wer seine Onboarding-Doku pflegt und trotzdem zusieht, wie sie ungelesen bleibt, hat das Rohmaterial bereits fertig. Der Rest ist Transformation, keine Produktion.&lt;/p&gt;

&lt;p&gt;Der pragmatische Test: das meistgefragte Doku-Kapitel exportieren und damit &lt;a href="https://leadde.ai/de/tools/ai-learning-video-generator" rel="noopener noreferrer"&gt;E-Learning-Videos selbst erstellen&lt;/a&gt;, mit ausgewogenem Preset und ausgefüllten Kontextfeldern. Nach einer halben Stunde liegt das erste Modul vor, und die Entscheidung fällt auf Basis eines echten Ergebnisses statt einer Produktseite.&lt;/p&gt;

&lt;p&gt;Bei uns hat genau dieses eine Testvideo den Ausschlag gegeben. Die anderen 21 kamen, weil das erste gut genug war.&lt;/p&gt;

&lt;p&gt;Fragen zum Workflow gern in die Kommentare. Insbesondere Erfahrungen mit anderen Doku-zu-Video-Pipelines würden mich interessieren; unser Setup ist bewusst simpel gehalten, und ich bin sicher, da geht noch mehr.&lt;/p&gt;

&lt;p&gt;Ein Punkt, den ich als Nächstes testen will: die Analytics-Daten der Videos systematischer auswerten. Die Plattform zeigt pro Video, wie weit geschaut wird, und die Abbruchstellen unserer Onboarding-Module sind vermutlich die ehrlichste Doku-Review, die wir je hatten. Wo Leute aussteigen, ist entweder das Video zu lang oder das zugrunde liegende Kapitel schlecht strukturiert. Beides ist ein Fix im Quelldokument, kein Video-Problem. Wenn sich daraus etwas Brauchbares ergibt, schreibe ich einen Follow-up-Post.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>video</category>
    </item>
    <item>
      <title>My 30-minute monthly ritual for picking dev tutorial topics</title>
      <dc:creator>douzatan</dc:creator>
      <pubDate>Sat, 01 Aug 2026 15:27:16 +0000</pubDate>
      <link>https://dev.to/douzatan/my-30-minute-monthly-ritual-for-picking-dev-tutorial-topics-3e6n</link>
      <guid>https://dev.to/douzatan/my-30-minute-monthly-ritual-for-picking-dev-tutorial-topics-3e6n</guid>
      <description>&lt;p&gt;I make developer tutorials on the side, and for a long time my topic pipeline was "whatever bug I fought last." That produces the occasional hit and a lot of misses, because the bugs I happen to hit aren't a representative sample of what my audience is stuck on.&lt;/p&gt;

&lt;p&gt;About a year ago I replaced the vibes with a ritual. Last Sunday of the month, coffee, thirty minutes, one page in my notes app. Here's the whole thing, phase by phase, in case it's useful to steal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 1 — Dump the candidates (5 minutes)
&lt;/h2&gt;

&lt;p&gt;I open a note and brain-dump every topic I could plausibly make a video about that month. No filtering. Bugs I fixed, questions from my Discord, a library I finally understood, something a coworker asked twice.&lt;/p&gt;

&lt;p&gt;The goal here is volume, not quality. I usually land on 12 to 20 candidates. Most will die in the next phase, and that's the point — this is the cheapest place to kill a bad idea, before it costs me a weekend of filming.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 2 — Pull real demand (10 minutes)
&lt;/h2&gt;

&lt;p&gt;This is the phase that changed my numbers, so I'll spend the words here.&lt;/p&gt;

&lt;p&gt;I take each candidate and check whether people actually search for it, and in what words. I use AllyHub's free tool to look up the &lt;a href="https://allyhub.com/tools/youtube-video-finder" rel="noopener noreferrer"&gt;YouTube most searched keywords&lt;/a&gt; around a topic — I type in a candidate like "react useEffect cleanup" and it shows me what people search near it, which phrasings pull more interest, and which videos currently surface for those queries.&lt;/p&gt;

&lt;p&gt;Two columns go in my note for each candidate: rough relative demand, and how strong the existing coverage looks. I'm not chasing precise numbers — the tool gives relative signal, not a view-count promise, and I treat it that way. I just want to know: is anyone looking for this, and is it already answered to death?&lt;/p&gt;

&lt;p&gt;A candidate I was sure about — "clean architecture in Node" — turned out crowded and well-served. One I nearly cut — "prisma migrate drift resolve" — showed steady interest with only scattered forum answers behind it. Column data flipped my instinct completely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 3 — Score and rank (5 minutes)
&lt;/h2&gt;

&lt;p&gt;Now I sort. My scoring is deliberately dumb: I want the intersection of real demand and weak existing coverage. High demand plus ten strong videos already there? Skip — I won't win that. Real demand plus stale or thin coverage? That goes to the top.&lt;/p&gt;

&lt;p&gt;I also flag anything with essentially zero search volume that I still want to make. More on those in a second. They don't get scored against the demand topics — they're a separate bucket.&lt;/p&gt;

&lt;p&gt;By the end of this phase I have a ranked shortlist of three or four topics, and I pick the top one or two to actually produce that month.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 4 — Reality-check with my own judgment (5 minutes)
&lt;/h2&gt;

&lt;p&gt;The data tells me what's wanted. It does not tell me whether I can make a good video about it, and I don't let it pretend otherwise. So the last five minutes are a gut check on each shortlisted topic: do I actually understand this well enough to teach it clearly, do I have a decent example on hand, will I still care about it once the camera's rolling.&lt;/p&gt;

&lt;p&gt;A topic can have great demand and still be a no for me because I'd make a shallow video. Demand is necessary. It's nowhere near sufficient. The quality is entirely on me once I hit record — no keyword tool is going to save a tutorial where I don't know the material.&lt;/p&gt;

&lt;h2&gt;
  
  
  The exemption I keep on purpose
&lt;/h2&gt;

&lt;p&gt;That separate bucket from Phase 3 matters. Some videos I make regardless of search demand, and I refuse to route them through this ritual.&lt;/p&gt;

&lt;p&gt;When I release an open-source tool, I make a walkthrough — nobody's searching for a thing that launched yesterday. When I have an opinion about where a framework is heading, I record it. When something breaks in a way that taught me something, I do a post-mortem. These are bets on trust and voice, not discovery, and running them through a demand filter would just talk me out of the work worth doing.&lt;/p&gt;

&lt;p&gt;So the ritual isn't "only make what people search for." It's "know which bucket a video is in." Discovery videos earn their spot through demand. Authority videos earn it through having something to say. The whole point of thirty minutes on the last Sunday is to stop confusing the two — which I did, expensively, for about two years before I wrote the ritual down.&lt;/p&gt;

</description>
      <category>youtube</category>
    </item>
    <item>
      <title>Your Hackathon Demo Video Has a Second Life — Measure It</title>
      <dc:creator>douzatan</dc:creator>
      <pubDate>Sat, 01 Aug 2026 15:02:59 +0000</pubDate>
      <link>https://dev.to/douzatan/your-hackathon-demo-video-has-a-second-life-measure-it-386j</link>
      <guid>https://dev.to/douzatan/your-hackathon-demo-video-has-a-second-life-measure-it-386j</guid>
      <description>&lt;p&gt;The weekend ends, the judges score, someone wins the API-sponsor prize, and everyone flies home. What most teams forget is that the two-minute demo video they recorded at 3 a.m. is still online, still indexed, and still answering a question people type into search months later. That video outlives the event by a wide margin. The event was one day. The video is a permanent artifact.&lt;/p&gt;

&lt;p&gt;Almost nobody measures the afterlife. Teams check the view count the Monday after — 200 views, mostly teammates and judges — and never look again. They miss the interesting part, which happens slowly and much later.&lt;/p&gt;

&lt;h2&gt;
  
  
  The demo is a landing page you already built
&lt;/h2&gt;

&lt;p&gt;Think about who finds a hackathon demo six months out. Not the judges. It is someone with the exact problem your hack solved, typing "how to X with Y API" into YouTube or Google and landing on your clip. That person is worth a hundred launch-day teammates, because they arrived carrying the problem your project addresses.&lt;/p&gt;

&lt;p&gt;If you shipped the project into a real product, that trickle is a lead source. If you're job hunting, it is a portfolio piece that keeps introducing itself while you sleep. Either way, the number you want isn't the total. It's whether new people keep arriving after the hype is gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  A flat total hides the only signal that matters
&lt;/h2&gt;

&lt;p&gt;Say the video sits at 1,400 views. Fine — but that lump tells you nothing about direction. Two very different stories produce the same 1,400:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A launch spike of 1,300 in week one from your own network, then near-zero. The video is dead; it only ever reached people you already knew.&lt;/li&gt;
&lt;li&gt;300 in week one, then a steady 30–40 a week for the rest of the year. Search found it. Strangers with the problem keep showing up.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The second video is the asset. The first is a memento. You cannot tell them apart from a cumulative count — you need the shape over time, the per-week velocity. That is the whole reason to track a curve instead of a total.&lt;/p&gt;

&lt;h2&gt;
  
  
  Snapshot on a schedule and let history accumulate
&lt;/h2&gt;

&lt;p&gt;The mechanics are dull, which is why they get skipped. You have to record the count on a fixed cadence so the gaps between readings become weekly velocity you can trust. Miss a few weeks and you are back to guessing.&lt;/p&gt;

&lt;p&gt;I set a small tool to &lt;a href="https://allyhub.com/tools/youtube-view-tracker" rel="noopener noreferrer"&gt;track YouTube video views&lt;/a&gt; on a daily snapshot for anything I want to watch long-term. It saves each reading, keeps the running history, and draws the per-day curve, so I'm looking at the slope rather than a single figure that only goes up. You configure it once and it keeps building the log on its own — no restart, no re-import when I check back in a quarter later. Daily is more than enough resolution here; a demo's afterlife moves in weeks, not seconds, and a live-updating counter would tell you nothing a once-a-day reading doesn't.&lt;/p&gt;

&lt;p&gt;For deep channel analytics — retention, traffic sources, where a viewer dropped — YouTube Studio is the right place, and better than any outside tool for your own uploads. The external snapshot solves a narrower problem: it works across any public video, including ones on a teammate's or the organizer's channel you don't own, and it keeps the comparison clean over long stretches.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the curve tells you to do next
&lt;/h2&gt;

&lt;p&gt;Read the slope and act on it. A demo that keeps pulling steady weekly views is telling you the topic has durable search demand — that is a signal to write the blog post, cut the follow-up video, or point the project's landing page at that keyword. A demo that spiked and flatlined is telling you the idea traveled through your network but never connected to a standing question people search for.&lt;/p&gt;

&lt;p&gt;Both are useful once the event is over and you're deciding where the project goes next. The hackathon graded your weekend. The view curve grades whether the thing you built keeps finding the people who need it.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Wie ich aus vorhandenen Schulungsunterlagen Lernvideos gemacht habe, ohne etwas zu filmen</title>
      <dc:creator>douzatan</dc:creator>
      <pubDate>Sat, 01 Aug 2026 07:31:38 +0000</pubDate>
      <link>https://dev.to/douzatan/wie-ich-aus-vorhandenen-schulungsunterlagen-lernvideos-gemacht-habe-ohne-etwas-zu-filmen-1m6c</link>
      <guid>https://dev.to/douzatan/wie-ich-aus-vorhandenen-schulungsunterlagen-lernvideos-gemacht-habe-ohne-etwas-zu-filmen-1m6c</guid>
      <description>&lt;p&gt;Unser Team hat brauchbare interne Schulungsunterlagen: Folien, ein paar PDFs, Notizen dazu, wie Dinge bei uns wirklich laufen. Was fehlte, war jemand, der sie sich ansieht. Neue Kolleginnen und Kollegen bekamen eine Foliensammlung ohne Kontext, überflogen sie und stellten dann trotzdem dieselben Fragen. Ein richtiges Lernvideo hätte geholfen, aber das von Hand zu produzieren, war nie realistisch. Also blieb es bei Text, den kaum jemand las.&lt;/p&gt;

&lt;p&gt;Ich habe ausprobiert, Lernvideos direkt aus den Unterlagen zu erzeugen, die wir ohnehin pflegen. Hier eine kurze Bilanz, ehrlich auch bei den Grenzen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Warum Folien als Lernmaterial schwach sind
&lt;/h2&gt;

&lt;p&gt;Folien sind dafür gebaut, einen Vortragenden zu stützen. Ohne die Person bleiben Stichpunkte, denen die wichtigste Ebene fehlt. Ein Dokument ist besser zum Nachschlagen, aber schlechter für den ersten Durchgang, weil kaum jemand ein System von oben nach unten durchliest, um es zu verstehen. Was wirklich einarbeitet, ist ein einmaliger Durchlauf im eigenen Tempo. Genau den hatten wir nie, weil Aufnahme und Schnitt pro Thema mehr Aufwand sind als die eigentliche Arbeit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Das Video aus dem Dokument erzeugen
&lt;/h2&gt;

&lt;p&gt;Ich wollte &lt;a href="https://leadde.ai/tools/ai-learning-video-generator" rel="noopener noreferrer"&gt;Lernvideos mit KI erstellen&lt;/a&gt; und habe dem Tool gegeben, was schon da war: die Folien als Text plus meine Notizen. Er hat eine Gliederung entworfen, die Szenen aufgebaut und die Vertonung erzeugt. Man wählt einen Erzählstil und einen Detailgrad und benennt die Zielgruppe. Ich habe erklärend, ausführlich, Zielgruppe „neue Entwicklerin" eingestellt, und das Skript hat den Stoff vermittelt, statt Stichpunkte vorzulesen.&lt;/p&gt;

&lt;p&gt;Entscheidend für einen Doku-Workflow: Die Eingabe ist das Dokument. Etwas Falsches korrigiere ich, indem ich den Text ändere und neu erzeuge, nicht durch eine neue Aufnahme. Als sich die Einrichtung letzten Monat geändert hat, war das Aktualisieren eine Sache von Minuten, kein Dreh. Für Leute, die den ganzen Tag an Code iterieren, fühlt sich das normal an. Die Unterstützung für viele Sprachen hieß außerdem, dass wir eine Version für nicht deutschsprachige Kolleginnen ausspielen konnten, indem wir das fertige Video übersetzen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wo es an Grenzen stößt
&lt;/h2&gt;

&lt;p&gt;Ehrlich: Der erzeugte Sprecher wirkt bei genauem Hinsehen leicht synthetisch. Für interne Schulung stört mich das nicht, für etwas nach außen Gerichtetes würde ich es abwägen. Es ist außerdem deutlich besser darin, Konzepte und Abläufe zu erklären, als eine echte Bildschirminteraktion zu zeigen. Für den Teil, in dem ich eine echte Terminal-Sitzung zeigen wollte, war eine Bildschirmaufnahme das richtige Werkzeug, nicht dieses. Und das Ergebnis spiegelt nur die Eingabe: Aus schludrigen Notizen wird ein schludriges Video, was nebenbei ehrliches Feedback zu den Notizen selbst war.&lt;/p&gt;

&lt;h2&gt;
  
  
  Was sich geändert hat
&lt;/h2&gt;

&lt;p&gt;Wir haben die Doku nicht ersetzt. Wir haben für die paar Themen, die neue Leute wirklich verinnerlichen sollten, kurze Videoversionen ergänzt, erzeugt aus den Unterlagen, die wir schon pflegen. Neue Kolleginnen sehen den Ablauf jetzt einmal während der Einarbeitung, statt ihm zum ersten Mal im Ernstfall zu begegnen, und die Videos bleiben aktuell, weil sie aus derselben Quelle neu erzeugt werden.&lt;/p&gt;

&lt;p&gt;Wenn ihr Schulungsunterlagen habt und immer vorhattet, Video zu ergänzen, es aber nie getan habt, weil es ein separater Aufwand ist, dann passt das hier. Nehmt das eine Thema, das am häufigsten Rückfragen auslöst, erzeugt aus dem vorhandenen Dokument eine Version im kostenlosen Tarif und legt sie in die Einarbeitung. Bei uns war es das Onboarding zum Deployment, und die zwanzig Minuten haben sich gelohnt.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Our Tutorial Videos Rotted Every Release, So We Stopped Filming Them</title>
      <dc:creator>douzatan</dc:creator>
      <pubDate>Sat, 01 Aug 2026 05:47:46 +0000</pubDate>
      <link>https://dev.to/douzatan/our-tutorial-videos-rotted-every-release-so-we-stopped-filming-them-hb6</link>
      <guid>https://dev.to/douzatan/our-tutorial-videos-rotted-every-release-so-we-stopped-filming-them-hb6</guid>
      <description>&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%2F9mekzonn2ksl1rtdpbyd.jpeg" 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%2F9mekzonn2ksl1rtdpbyd.jpeg" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every release used to break a video.&lt;/p&gt;

&lt;p&gt;We maintain a developer tool, and the short explainer videos attached to it stayed accurate for about six weeks. Then a flag got renamed, a panel moved, the default changed, and the video was quietly wrong. Nobody volunteered to re-record it, because re-recording meant blocking an afternoon, setting up a mic, and doing five takes to get ninety seconds without stumbling over the word "idempotent." So the wrong video stayed up until a user pointed it out in an issue, and then it usually got deleted rather than remade.&lt;/p&gt;

&lt;p&gt;The docs never had this problem. A doc changes in a pull request. Someone reviews the diff, it merges, and the site rebuilds. Video had no equivalent of that loop, so video rotted, and after the second round of rot we stopped making it. That is how a team ends up with docs that are current and a YouTube playlist that is a museum.&lt;/p&gt;

&lt;p&gt;What eventually fixed it was not better recording discipline. It was removing the recording step entirely, so the video became something we regenerate from text like everything else we ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why we wanted a presenter and not just a screencast
&lt;/h2&gt;

&lt;p&gt;The obvious answer is a screencast with a voiceover. We had those. They are fine for "here is the sequence of clicks," and they are genuinely better than a talking head for anything where the user needs to see the actual interface respond.&lt;/p&gt;

&lt;p&gt;They are worse for the conceptual half. When the video is explaining why a feature exists, what the mental model is, or which of two approaches you should pick, a cursor moving around a screen while a disembodied voice talks is hard to stay with. I notice it in my own viewing: I keep the tab open, I stop watching, I skim the transcript instead. A face delivering the explanation holds me longer. I do not have clean numbers from our own content to prove this, and I am suspicious of anyone who claims they do, but the pattern was consistent enough that we kept wanting the presenter format and kept failing to produce it.&lt;/p&gt;

&lt;p&gt;The reason we failed was mundane. A talking head requires a specific human being to be available, on camera, on a schedule. On a small team, that human is also the person shipping the feature the video is about. Their time is the constraint, and video loses to shipping every single time. The format was not too hard for us. It was too dependent on a calendar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating the presenter from the script instead
&lt;/h2&gt;

&lt;p&gt;The change was using an &lt;a href="https://leadde.ai/tools/ai-talking-head-generator" rel="noopener noreferrer"&gt;AI talking head generator&lt;/a&gt; to produce the presenter, so the input is a script rather than a recording session.&lt;/p&gt;

&lt;p&gt;The mechanics are dull, which is the point. You paste the script, pick a presenter from the built-in library, choose a voice, and it generates the talking-head video with lip sync. There is a setting for more natural expression so the delivery does not come out flat. You can also generate an avatar from a single photo if you want a consistent face that is specifically yours rather than one from the library, and you can put your own background behind it or drop in a brand kit so the clips in a series match. Output is an MP4 you download, plus a hosted link if you want to embed it.&lt;/p&gt;

&lt;p&gt;The part that mattered for us: fixing a line means editing text and regenerating. Not booking a re-record. Not asking a teammate to sit down again for one sentence. That single property is what turned video from a project into a build step. When we renamed a flag last quarter, updating the video was a two-line diff in a script file and a regenerate, and it was done before the release notes were finished.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scripts live in the repo now
&lt;/h2&gt;

&lt;p&gt;Once video generation takes text as input, the script is source, so we treat it that way.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docs/
  videos/
    01-getting-started.script.md
    02-config-file.script.md
    03-ci-integration.script.md
    README.md          # voice, presenter, aspect ratio per series
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scripts get reviewed in a pull request like anything else. If a PR changes a CLI flag and there is a script mentioning that flag, the reviewer sees both in the same diff, which is the thing that never happened when video lived in a folder on someone's laptop. We do not have automated generation wired to CI, and I would not pretend otherwise: someone still opens the browser, pastes the updated script, and regenerates. But the expensive part is gone, and the cheap part is a five minute chore rather than an afternoon.&lt;/p&gt;

&lt;p&gt;A few things we learned about writing these scripts, none of which are obvious the first time:&lt;/p&gt;

&lt;p&gt;Write for the ear, not the eye. Sentences you would happily publish in a doc are often too long to listen to. Read the script out loud once before generating and cut anything you run out of breath on.&lt;/p&gt;

&lt;p&gt;Do not read code aloud. A synthesized voice narrating &lt;code&gt;--max-retries=3&lt;/code&gt; is unpleasant, and it is unpleasant when a human does it too. Say what the flag does, show the code on screen, and let the two work together.&lt;/p&gt;

&lt;p&gt;Say numbers the way people say them. Written "v2.4.0" gets voiced in ways you will not like. Spell it out phonetically in the script and let the doc keep the clean version.&lt;/p&gt;

&lt;p&gt;The scripts also gave us something we did not plan for. Because our docs are translated, we can generate the same video in other languages from a translated script with the same presenter, which is a lot more approachable than asking someone to re-record a tutorial in a language they do not speak.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it is genuinely not good
&lt;/h2&gt;

&lt;p&gt;This puts a synthetic face on screen, so being straight about it matters more than usual for a dev audience, which is not a crowd that responds well to being sold to.&lt;/p&gt;

&lt;p&gt;It reads as generated. Not badly, but if you look at it with intent, you can tell it is not filmed. For routine explanatory content that is fine and nobody has complained. For anything where you are the point, a conference talk, a project announcement you want to feel personal, a video where a user is deciding whether to trust you as a maintainer, film yourself or do not make the video.&lt;/p&gt;

&lt;p&gt;It does not show your interface. The presenter explains, it does not demonstrate. For the click-by-click sections we still capture the screen with our own recorder and upload that footage, then shrink the avatar into a corner so the interface has the frame. Anyone expecting the tool to record their screen for them is expecting the wrong tool.&lt;/p&gt;

&lt;p&gt;The output is only as good as the script. Our first attempt was lifeless because we fed it a doc page verbatim, and a doc page is written to be scanned, not spoken. Rewriting it as something a person would actually say fixed it. The tool has no idea what your users are confused about unless the script says so.&lt;/p&gt;

&lt;p&gt;And on likeness: only build an avatar from an image you have the right to use. If it is a teammate's face, that needs their explicit yes, and the yes needs to cover the videos you are actually going to make with it. Putting a colleague's face on a video they never agreed to is not a shortcut worth taking.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you want to try this
&lt;/h2&gt;

&lt;p&gt;Do not migrate a whole channel. Pick the one video you already know is out of date and have been avoiding for a month, write the script as a real script rather than a pasted doc page, and generate a couple of versions on a free tier. Watch them with someone who has not read the script.&lt;/p&gt;

&lt;p&gt;If the presenter reads as a deliberate choice rather than an accident, keep it for that format. If it reads as off, you have lost an hour and learned something. For us the test case was the getting-started video, the one that broke on literally every release, and it has been current for three releases running, which it never managed when it depended on somebody being free to film.&lt;/p&gt;

</description>
      <category>devrel</category>
      <category>documentation</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How I Pick Which Coding Lessons to Record, After Wasting a Term on the Wrong Ones</title>
      <dc:creator>douzatan</dc:creator>
      <pubDate>Wed, 29 Jul 2026 03:43:29 +0000</pubDate>
      <link>https://dev.to/douzatan/how-i-pick-which-coding-lessons-to-record-after-wasting-a-term-on-the-wrong-ones-407f</link>
      <guid>https://dev.to/douzatan/how-i-pick-which-coding-lessons-to-record-after-wasting-a-term-on-the-wrong-ones-407f</guid>
      <description>&lt;p&gt;I teach web development, partly in a bootcamp and partly on a YouTube channel that started as lecture overflow. For my first full term of recording, I chose topics the way most instructors do: I taught what I thought was foundational, in syllabus order. Syllabus order and "what people actually get stuck on" order turned out to be almost unrelated, and my analytics said so, loudly.&lt;/p&gt;

&lt;p&gt;Key takeaways&lt;br&gt;
Pick tutorial topics from observed search demand, not from syllabus order or personal interest.&lt;br&gt;
Collect the exact phrases learners type, typos included, then check which ones people actually search.&lt;br&gt;
Chase the overlap: real search demand plus weak or dated coverage.&lt;br&gt;
Keyword research decides what to make, not whether your teaching is any good. Those are separate problems.&lt;br&gt;
The term I wasted on syllabus order&lt;br&gt;
My carefully sequenced fundamentals playlist was a graveyard with two or three accidental hits. The hits confused me most, because I could not explain them, which meant I could not repeat them. A content library shaped by the syllabus mostly serves the syllabus.&lt;/p&gt;

&lt;p&gt;The accidental hit that changed my process&lt;br&gt;
I recorded a five-minute throwaway on a CORS error because a student hit it twice in one week. I almost did not post it. It has more views than my entire fundamentals playlist combined, and the comments are all one sentence: thank you, this was the only clear explanation I could find.&lt;/p&gt;

&lt;p&gt;Step 1: collect the phrases learners actually type&lt;br&gt;
Now I keep a running note of every question I see learners ask, in Discord servers, in subreddit threads, and under my own videos. I add the error messages and "how do I" moments from my own week. No filtering yet. The list runs 15 to 20 phrases, written the way people actually typed them, typos preserved. The typos are the real vocabulary.&lt;/p&gt;

&lt;p&gt;Step 2: check the phrases against real search demand&lt;br&gt;
Before I record anything, I spend twenty minutes running that list through a YouTube keyword research tool to see which phrasings people search around a topic and what already surfaces for them. This step deletes about two thirds of my list, which is the point.&lt;/p&gt;

&lt;p&gt;The two gaps worth chasing&lt;br&gt;
First, the vocabulary gap. I teach "asynchronous control flow." Nobody stuck at midnight searches that. They search "why is my await not waiting." I now keep a small glossary mapping my teaching terms to the searched phrasings, and I use the searched version in the title.&lt;/p&gt;

&lt;p&gt;Second, the coverage gap. Some of the most-searched beginner errors are answered only by videos years old, using syntax two versions out of date. That is not competition. That is an opening, and it goes to the front of my queue.&lt;/p&gt;

&lt;p&gt;One honest limit&lt;br&gt;
This tells you what to make, not whether your explanation lands. I have posted well-researched topics with mediocre teaching and watched them underperform a rushed video on a hotter question. Selection gets you found. The teaching still has to be good, and no keyword tool fixes that half for you.&lt;/p&gt;

&lt;p&gt;Try it before your next recording&lt;br&gt;
If your channel has a graveyard and a couple of mystery hits, spend twenty minutes on demand before your next video. Worst case, you confirm your instinct. Best case, you stop making videos nobody was ever going to search for.&lt;/p&gt;

</description>
      <category>youtube</category>
      <category>teaching</category>
      <category>webdev</category>
      <category>contentcreation</category>
    </item>
  </channel>
</rss>
