← Back to field notes
Infrastructure6 min readSep 26, 2026

Teaching Siri to Speak Mist

Building native App Intents integration for MistPulse: a parallel Swift API client, its own build line, and the real TestFlight failures that came with going native.

Part 5 of 10 in Building MistPulse: A Network Architect's Field Notes

Share:XLinkedIn
Teaching Siri to Speak Mist

There's a specific kind of troubleshooting moment this project was built for: hands full, phone in a pocket, and a simple question: is the site's SLE health still bad? Typing that into an app, even a fast one, is friction I wanted to remove entirely. The answer was Apple's App Intents framework and SiriKit: letting a voice command reach directly into MistPulse's data without opening the app at all. It sounded like a small feature. It turned into one of the most technically demanding parts of the entire project.

Why this couldn't just be JavaScript

React Native and Expo get you a long way, but Siri integration lives below that layer, in native iOS territory. App Intents are a Swift framework, and hooking them up meant writing a genuine native Swift module, not a wrapper around existing app code, but a parallel implementation that could run independently of the React Native runtime, since Siri can invoke an intent even when the app itself isn't running in the foreground. That meant a second, smaller version of the Mist API client had to exist purely in Swift, alongside the TypeScript one already powering the app.

Siri can invoke an intent even when the app isn't running, which means the Mist API client had to exist twice, in two languages, and stay in sync.

This is a real maintenance cost, and I want to be honest about it rather than gloss over it: any change to how MistPulse talks to the Mist API, a new endpoint, a changed response shape, an authentication tweak, now has to be considered on both sides. The TypeScript client that the main app uses, and the Swift client that the Siri module uses independently. Keeping those two in sync deliberately, rather than letting them drift, became its own discipline layered on top of the API-validation methodology from earlier in this series.

Two clients, one API: the React Native TypeScript client and the Siri App Intents Swift module, kept in sync manually

A separate build line, on purpose

Because the Siri module is a distinct native target with its own compilation and its own way of failing, I set it up as a separate build line from the main app: its own build numbering, tracked independently through Xcode and TestFlight. This wasn't just organizational tidiness. Native module builds fail in ways that JavaScript changes never do: signing issues, entitlement mismatches, App Intents metadata that Xcode silently refuses to bundle correctly, warnings during upload that turn out to be blocking errors in disguise.

Several real TestFlight build attempts failed during this phase, not because the Swift logic was wrong, but because of exactly this class of native-build friction: an upload warning that looked cosmetic but actually meant a required entitlement was missing, or an App Intents definition that needed a specific Info.plist configuration I hadn't set correctly on the first pass. None of this shows up by reading Swift code carefully. It only shows up by actually running an archive and upload cycle and reading Xcode's sometimes-cryptic feedback.

  • App Intents live below the JavaScript layer: this required a genuine native Swift module, not a wrapper
  • The Mist API client exists twice: once in TypeScript for the app, once in Swift for Siri, kept in sync deliberately
  • The Siri module has its own build line and version numbering, separate from the main app
  • Real TestFlight upload failures during this phase were almost all native configuration issues, not logic bugs

What Siri can actually do

The scope for voice commands mirrors the same philosophy from Part 1: narrow, fast, read-heavy. Ask about a site's current SLE health, and get a spoken or on-screen summary without unlocking the phone and navigating anywhere. It's deliberately not trying to be a full conversational interface into every corner of Mist: that would be both a much larger engineering effort and, frankly, a worse user experience than a few sharp, reliable voice shortcuts that always work the same way.

Designing for "always works the same way" mattered more here than almost anywhere else in the project. A dashboard screen that's occasionally slow to load is a minor annoyance. A Siri shortcut that sometimes silently fails, or returns a stale answer, erodes trust in the feature entirely: a voice assistant only gets used if it's dependable enough that reaching for it becomes a reflex rather than a gamble.

The lesson that carried into everything after

Building the Siri integration was, in a real sense, a forcing function for maturity in the rest of the codebase. Having a second client depend on the same underlying Mist data meant that any looseness in how the main app modeled entities, sites, devices, metrics, got exposed the moment the Swift side had to model the same things independently. That tension is exactly what set up the next challenge, and arguably the trickiest one in this whole project: getting a voice command like "check the site called Home" to reliably resolve to the correct real object in Mist, every time, without the user having to speak an exact, unambiguous identifier.

Coming up next: The Entity Resolution Saga: how MistPulse matches a spoken name to the right site or device, and the false starts along the way

J

Written by

Jesus Hurtado

Network Architect and Cybersecurity Professional.

Comments

No comments yet. Be the first.

Leave a comment