Back to Blog

Designing agentic iOS features with clear user control

A practical model for agentic app features: explicit user intent, bounded tools, visible progress, confirmation points, and recoverable actions.

6 min read

Agentic features change how iOS apps interact with users, but they do not change the fundamental rules of trust.

When an app transitions from executing simple commands to executing complex multi-step sequences on behalf of the user, the risk of failure increases exponentially. If the app acts on outdated local data, misinterprets the user’s intent, or makes irreversible changes without verification, the user will not think the app is smart. They will think it is unsafe to use.

A production-grade agentic feature needs more than an integration with Apple Intelligence or a server-side LLM. It needs a structured runtime that enforces explicit boundaries, keeps the user in control, and fails gracefully when things go sideways.

1. Ground the loop in explicit user intent

An agentic loop should never begin on a guess.

While background intelligence can suggest possibilities, any action with consequences must be initiated by direct, unambiguous user action. This is the difference between a helpful assistant and an autonomous machine that rearranges your files when you are not looking.

In practice, starting with clear intent means:

  • Defining a strict entry command or interaction target (e.g., clicking a specific “Generate Plan” button or matching a precise vocal trigger).
  • Capturing the raw user input and keeping it immutably attached to the session context.
  • Generating a clear, human-readable summary of what the loop is about to do before the first tool call runs.

If the user asks to “organize my inbox for tax season,” the app’s first step is to clarify the goal: I will search for receipts from the last 12 months, tag them ‘Tax 2026’, and export them to your shared folder.

Only when the user clicks “Start” does the execution loop begin.

2. Bind the tools to a deterministic interface

The model should never have direct access to app services, file paths, or system resources.

Instead, the model must interact with the app through a discrete, strongly-typed tool interface. Each tool is a contract: it takes a specific Swift struct as input, runs deterministic validation, executes the work within the app’s normal data layers, and returns a verified result structure.

struct MessageTagsTool {
    struct Input: Codable {
        let messageIDs: [UUID]
        let tagName: String
    }
    
    struct Output: Codable {
        let taggedCount: Int
        let failures: [UUID]
    }
    
    func execute(input: Input, in context: AppContext) async throws -> Output {
        // Enforce deterministic app rules, sandbox access, and permissions
        guard context.currentUser.hasPermission(.editTags) else {
            throw ToolError.unauthorized
        }
        
        let result = try await context.database.applyTag(input.tagName, to: input.messageIDs)
        return Output(taggedCount: result.successes, failures: result.failures)
    }
}

By decoupling the model’s reasoning from direct execution, you gain three critical safeguards:

  1. Security: The database rules, OS permissions, and user credentials remain in your control. The model cannot bypass validation because it only emits a representation of a tool call.
  2. Observability: You can log, trace, and audit every input and output structure. If an agent misbehaves, the logs will show exactly which tool parameters were generated.
  3. Simulation: You can dry-run the tool calls. You can pass the model’s output through a simulator to show the user exactly what changes would happen before they actually modify any database rows.

3. Expose progress with granular visibility

If an agentic loop takes longer than two seconds, silence is a bug.

A run-on loop that spins without status updates makes the user assume the app has locked up or crashed. More importantly, it hides what the agent is doing, which breeds suspicion.

Always design the interface to stream the agent’s execution sequence in real time:

  • Use clear, short, action-oriented status states (e.g., “Searching attachments…”, “Reading receipt details…”, “Applying tags…”).
  • Map each tool execution phase to a visible step in a progress list.
  • Keep the interface responsive so the user can pause or cancel the sequence mid-flight.

If the agent is evaluating thirty emails, show the list. Show the progress bar moving. Let the user see that the app is systematically working through the task, not guessing blindly.

4. Require explicit verification for high-risk actions

Not all actions are equal.

Adding a tag to an email is a low-risk, reversible action. Transferring funds, deleting files, sending an external email, or pubishing an update are high-risk, irreversible actions.

Enforce a hard boundary in your runtime:

  • Define a set of “protected” tool types that cannot execute autonomously.
  • When the agentic loop encounters a protected tool (e.g., PublishArticleTool or DeleteRecordTool), the loop must pause.
  • Present a native iOS sheets or confirmation prompt displaying the exact parameters the agent wants to submit.
  • Require consecutive manual confirmation (like a Swip-to-Confirm or double-tap) before the runtime executes the protected tool.

This is the “human-in-the-loop” pattern reduced to code. The agent did the heavy lifting of research, aggregation, preparation, and drafting. But the final decision to commit remains with the human being who owns the data.

5. Design every action to be recoverable

Even with confirmation, users make mistakes, and models make mistakes.

If your agentic feature moves 50 files into a new folder, and the user realizes the result is wrong, fixing it manually is an annoying penalty. If they cannot fix it, the feature is a liability.

Make recoverability a first-class citizen in your agentic architecture:

  • Track the transaction history of the agentic session inside a local store.
  • Group multiple small actions into a single “session transaction.”
  • Expose a clear, temporary “Undo” option when the loop completes.
  • Implement an inverse execution pass for all standard tools (e.g., MessageTagsTool has a corresponding RemoveMessageTagsTool).

If the user undoes the session, the app reads the session transaction log in reverse order, applies the inverse tools, and restores the data to its exact state before the agent started.

6. Build the backup path first

The smartest agent is still a statistical model. It will get stuck in loops. It will generate malformed arguments. It will hit system limits, rate limits, or network timeouts.

Do not try to make the model perfect. Instead, build your app to handle the model’s inevitable failures gracefully.

If a tool call fails three times, abort the loop and show the error. If the input parameters are incomplete, fallback to a standard native SwiftUI form pre-filled with what the model did figure out, and let the user finish the job.

An agent that fails gracefully and lets you complete the task manually is a tool. An agent that crashes, hangs, or ignores its boundaries is an obstacle. Keep the boundary clear, and the user will keep using the tool.