From requirements to implementation: designing tasks for AI agents


Greetings, traveler!

“Add iCloud sync” sounds like a reasonable task for an AI coding agent. A SwiftData app already saves data locally, and the framework can synchronize a compatible store through CloudKit. The agent could find the model container, change its configuration, adjust the entitlements, and produce a build.

But what should happen to the data that already exists on a user’s phone? What if the same person has been using the app independently on two devices? Does turning sync off remove anything from iCloud? What should the app display when no iCloud account is available?

Those questions are part of the feature. If nobody answers them, an agent still has to make choices, whether explicitly or by leaving behavior undefined. The resulting code may compile and even synchronize data in a basic test while failing to meet the product’s needs.

A useful way to work with an agent is to separate three activities: understanding the problem, deciding what the product should do, and planning how to change the code. They inform one another, and you may revisit earlier decisions as you learn more. Keeping their outputs distinct makes it easier to see which questions are settled and which still need investigation.

Let’s walk through that process using a hypothetical habit-tracking app. It uses SwiftData, stores habits and completion entries locally, and has existing users. We want to add optional synchronization between a person’s Apple devices.

Start with the outcome, not the implementation

An initial request often describes a solution before the problem is fully understood:

Add a CloudKit toggle to Settings and sync the SwiftData store.

This tells the agent where to put a control and suggests a technology. It does not say what the control promises or what must happen to existing data. A more useful starting brief might be:

People who use the app on more than one Apple device should be able to access their habits and completion history on those devices. The app must remain usable offline, and enabling sync must not silently discard existing local data. Investigate the current persistence setup and propose a design before changing it.

This is still incomplete. That is fine: a starting brief should give the investigation direction, not pretend every decision has already been made.

The agent can help turn it into a better-defined task. Ask it to inspect the code and report what it finds about store configuration, model relationships, schema versions, existing migration tests, and the settings flow. It should identify relevant files and distinguish observed facts from assumptions. If the repository does not answer a product question, the agent should bring that question back rather than infer a policy from a class name.

For example, the code may show that each installation creates its own habits. It cannot tell us whether two habits called “Morning walk” on different devices represent one habit or two. That requires a product decision.

Investigate before asking for a plan

Exploration is useful when it produces decisions, not just a long list of possible edge cases. A focused discussion with the agent might begin with these questions:

  • Which models must sync? Are settings, attachments, and archived habits included?
  • Can users create and edit data while offline?
  • What happens when sync is enabled on a device with existing local data?
  • What happens if the iCloud account already contains data from another installation?
  • What does “turn off sync” mean for local and cloud data?
  • How will the app behave when iCloud is unavailable or the device changes accounts?
  • Which outcomes would count as data loss, duplication, or an unacceptable conflict?

The agent should first answer what it can from the project, then ask the developer about choices that require product knowledge. Questions are more useful when accompanied by consequences. For instance: “Should disabling sync delete cloud data?” matters because deletion on one device may affect other devices. The answer should not be hidden inside an implementation detail.

Some questions need an experiment. A small, disposable spike can check whether a copy of the current model is accepted by a CloudKit-backed configuration, or whether an existing store opens after a proposed schema change. A spike can use test data and a development container. Its purpose is to reduce uncertainty; a successful spike is not proof that production migration is safe.

Keep a short record of the result: what was tested, the environment, the observed outcome, and what remains unknown. If the experiment changes the assumptions behind the design, update the design before expanding the implementation.

Make decisions explicit

Suppose the team chooses the following scope for the first release:

  • Habits and completion entries sync between devices using the same iCloud account after the user opts in on each device.
  • The app remains usable without a network connection or an available iCloud account. Local edits remain available on the device.
  • Turning sync off stops the app’s participation on that device after a safe transition. It retains the local copy and does not request deletion of cloud data.
  • Cloud-data deletion, sharing with other people, and a manual conflict-resolution screen are outside this release.
  • Previously independent datasets must not be silently merged based on matching titles. Records that cannot be identified as the same logical item remain separate until a product-approved reconciliation rule exists.

These are example product decisions, not automatic SwiftData guarantees. In particular, opting in or out safely requires a design for store configuration, transition, restart behavior, and recovery from failure. Adding a Boolean setting alone does not establish those properties.

There are still open questions. For example, can the existing store be migrated safely into the selected sync configuration on every supported app version? What happens when a user signs out of iCloud and signs in to a different account? Until investigation answers them, mark them as unresolved and decide whether they block implementation or release. For the account-change case, a conservative release condition would be to preserve local data and prevent an unverified transfer to the new account; the exact flow still needs design and testing.

The transition needs particular attention when there are edits that have been saved locally but not yet exported. The team should decide how to preserve them, what to do if the transition is interrupted, and how to verify that the app reopens the intended store rather than creating an empty one.

Turn requirements into observable behavior

“Sync should be reliable” is a valuable intention, but it is too vague to review. Acceptance criteria make the intended behavior observable under specific conditions.

ScenarioExpected behaviorHow to check
No networkA user can add and edit a habit locally; the change remains after relaunch.Disconnect a test device, edit a habit, terminate and reopen the app.
Existing local dataEnabling sync preserves the pre-existing habits and completion entries.Upgrade a seeded installation, enable sync, and compare records before and after the transition.
Two opted-in devicesA habit created on device A appears on device B after connectivity returns and synchronization completes.Use two test devices on the same test account; poll for the expected record within the test’s timeout and inspect logs if it does not arrive.
Sync disabledAfter a safe transition, data remains on A. New edits on A stay local; remote edits are not applied on A. Disabling sync does not request cloud deletion.Disable on A and relaunch it. Edit separate records on A and B, then check both devices during a defined observation period. Inspect available diagnostics and cloud state for unintended deletion.
No iCloud accountThe user can continue using local data and receives an explanation that sync is unavailable.Sign out on a test device, launch the app, and try to enable sync.
Account changes after opt-inThe intended behavior still needs a product and engineering decision; preservation of local data is a release requirement.With saved but unexported edits, sign out or change accounts. Check local records and pending changes, and verify the agreed behavior before and after relaunch.

A timeout in a test is a practical boundary for that test, not a guarantee that CloudKit delivers within that interval. Synchronization happens asynchronously and depends on system scheduling and conditions. The app should not promise an exact completion time unless it can actually support that promise.

Similarly, observing no changes on the other device for a set period does not prove that a delayed operation will never run. Combine the two-device check with whatever diagnostics the chosen implementation exposes. Cloud state and logs can help investigate a deletion, although their availability and detail depend on the test environment.

Conflict behavior needs the same care. Consider a habit whose title is changed on two offline devices. “The latest change wins” sounds precise until we ask whether “latest” means device clock time, save order, or server processing order, and what happens when clocks differ. Nor should we assume that automatic SwiftData synchronization exposes an application-defined conflict policy for every case.

For this release, the team could decide that simultaneous edits to the same field are not guaranteed to preserve both values, while distinct existing records must not be deleted or deduplicated by title. The team should then test the actual behavior of the chosen stack and decide whether it is acceptable. If preserving both edits is a hard requirement, that is an architectural constraint to investigate before promising the feature, potentially requiring a different data model or synchronization approach.

For one test, disconnect both devices, give the same habit a different title on each, and reconnect them. Once synchronization has had an opportunity to complete, inspect the title and other fields on both devices, then relaunch and check again. Record the observed result and compare it with the agreed requirement; a single run does not establish a universal conflict policy.

Acceptance criteria are not a substitute for implementation tests. They define outcomes that tests and manual checks can examine. The criteria should also name any important limitations, so a passing test does not imply more than it establishes.

Compare approaches against the existing app

Once the expected behavior is clearer, ask the agent for a small number of viable approaches and their consequences.

SwiftData’s automatic CloudKit synchronization reuses the existing persistence framework and avoids building a custom synchronization protocol. The schema must be compatible, though, and the team still needs to investigate store transitions, existing data, account changes, and conflicts. The system controls when synchronization takes place.

A custom synchronization layer using CloudKit gives the team more control over record operations and application-specific reconciliation. It also makes the team responsible for change tracking, retries, conflict handling, deletions, migrations, and the tests around them.

For a local-first habit tracker, the automatic approach is a reasonable candidate to investigate first. That recommendation depends on whether its behavior can meet the requirements. If the team later discovers that a mandatory conflict policy or account transition cannot be implemented safely, the decision should be revisited.

The current schema matters. CloudKit-backed SwiftData has compatibility constraints: unique constraints are not supported, relationships must be optional and need correct inverses, and the deny relationship delete rule is unsupported. SwiftData can infer an inverse in some cases; otherwise the model needs an explicit one. Existing identifiers and migrations deserve inspection too. CloudKit schema changes also have production constraints, so development and production rollout should be planned separately.

The agent can point out incompatibilities, but it should not automatically replace a unique constraint and claim the original invariant is preserved. The team must decide where that invariant belongs and how to verify it when devices make changes independently.

Write a brief the implementation can follow

After exploration, capture the agreed outcome in a concise feature brief. It does not need a particular template, but it should make the boundaries visible. For this feature, it might contain:

Goal
Make habits and completion history available across a person's opted-in devices.

Scope
Existing local SwiftData app; private data for the same iCloud account.

Must preserve
Local access while offline; existing records across upgrade and opt-in.

Product decisions
Sync is opt-in per device. Turning it off retains local data and does not
request cloud deletion. No title-based automatic deduplication.

Out of scope
Sharing, cloud-data deletion controls, and manual conflict-resolution UI.

Open questions / blockers
Prove a safe store transition for supported versions. Define and test the
behavior when the iCloud account changes. Evaluate concurrent edits.

Verification
Upgrade from seeded stores; test offline edits, two-device propagation,
disable/relaunch, missing account, account change, and recovery after failure.

Link this brief to the relevant issue or keep it alongside the change if that fits the team’s workflow. If the design changes, update the brief rather than leaving conflicting instructions scattered through a conversation.

An agent can draft the brief and flag contradictions. The developer or product owner still needs to decide questions that affect the user. Agreement does not require a ceremony for every change, but an irreversible data transition deserves a clear decision from the people responsible for it.

Break the work into changes that can be checked

A plan should connect each change to an outcome and a way to examine it. “Implement CloudKit, then test” hides too much. A more useful plan for this example might look like this:

StepChange and dependencyCheck before continuing
1. Map the current storeIdentify schema versions, migration paths, relationships, and seeded upgrade fixtures.Reopen representative existing stores and record the baseline data.
2. Test feasibilityIn a development-only spike, evaluate model compatibility, transition options, and concurrent edits. Depends on step 1.Document observed behavior and unresolved risks; stop if the required data guarantees cannot be met.
3. Prepare the modelMake only the schema changes required by the selected design and provide any necessary migration. Depends on the feasibility decision.Run model and upgrade tests against seeded stores; verify records and relationships survive.
4. Implement the transitionAdd the selected opt-in and opt-out flow, including failure recovery and persisted state. Depends on the validated model and transition design.Interrupt the transition at relevant points; verify that a relaunch does not silently lose local data or report a false success.
5. Add the settings experiencePresent the choice, availability, and actionable errors using the behavior defined in the brief. Depends on the transition contract.Check enabled, disabled, unavailable, and failure states; verify wording matches actual behavior.
6. Validate integrationConfigure development and production requirements and exercise the complete workflow. Depends on earlier steps.Run the scenario matrix on appropriate devices and accounts; review migration and release readiness.

These steps can produce separate reviewable changes, but they are not necessarily independent. The settings screen depends on what the transition can truthfully report, and the transition depends on a viable store design.

Each step should leave the project in a state appropriate for its destination. An experimental branch may contain a throwaway spike; a change intended for release should not expose a nonfunctional toggle merely because the UI was easy to build first. If a partially completed feature must be integrated, define how it remains inaccessible until the complete path is ready.

Give the agent a bounded task

Once the brief is agreed, a request for the first implementation step can be specific without dictating every line of code:

Inspect the SwiftData models and persistence configuration for CloudKit
compatibility. Identify existing schema versions and upgrade paths.

Use the agreed feature brief as the product scope. Do not enable sync or
change production entitlements in this step.

Report:
- relevant files and the current store configuration;
- model constraints that could prevent automatic synchronization;
- migration and data-preservation risks;
- a proposed feasibility experiment with test data;
- open questions that require a product or engineering decision.

Separate findings from assumptions. Run existing relevant tests if available
and report the exact commands and outcomes. If a test cannot run, say why.

This request gives the agent enough freedom to discover the project while making the expected output and boundaries clear. It also avoids asking for a full implementation before the feasibility questions have been answered.

A later implementation task can reference the approved design and the relevant plan step. Include expected behavior, dependencies, tests, and constraints on unrelated changes. Prefer concrete project paths once discovery has established them; invented paths in an initial plan can send the agent toward the wrong architecture.

Treat the plan as a working document

Even a good plan contains assumptions. During implementation, the agent might discover a model migration that the proposed approach cannot handle or a framework behavior that contradicts the expected opt-out flow.

The right response is to surface the discrepancy, preserve the evidence, and revise the affected decision. For a small local issue, the agent may be able to propose a correction and continue within the agreed scope. A change that weakens data-preservation guarantees, alters the product behavior, or requires deleting user data needs a decision from the responsible person before proceeding.

Progress reports should make it possible to tell the difference between “implemented,” “checked,” and “still unknown.” For example, a successful unit test of a settings state does not prove that records synchronize between two devices. Conversely, a successful two-device demonstration does not prove that every historical store version migrates correctly.

At the end, compare the result with the brief and acceptance criteria. Review the actual diff, the results of the checks, and any scenarios that could not be tested. A feature is ready when its required behavior has been demonstrated to the level the team considers appropriate for the risk, and the remaining limitations are understood and accepted.

Scale the process to the change

A one-line accessibility-label correction usually does not need a separate design document. The expected outcome, affected screen, and a quick verification may fit in a short message. A change to persistent data, payments, authentication, or account behavior deserves more investigation because an incorrect assumption can be costly to reverse.

The amount of planning should follow uncertainty, impact, and the cost of correction. Start with what is known. Investigate the decisions that could change the implementation or harm users if guessed incorrectly. Then give the agent a task whose outcome can be reviewed.

Before implementation, identify the uncertainties that could change the design or put existing data at risk. Use those questions to decide what to investigate first.