Most of the effort in building an app used to go into writing the code. With an agent doing the building, that effort moves to describing the work precisely up front and reviewing what comes back.
This tutorial walks you through building a small contact list app for desktop and web that way. You draw the design, write a handful of instruction files that tell the agent how you want code built, feed it six prompts, and never open a source file until the end. By the end you will have a running app, a set of reusable skills you can point at your next project, and a test suite and cleanup pass that keep the generated code in check. As usual, you can follow along with the written tutorial below or the video from the livestream.
What’s New This Time
An earlier version of this exercise used code generation inside ChatGPT Canvas and still needed hand edits to get a horoscope app working. Canvas is gone, and this build needed none. Three pieces make the difference:
Skills are Markdown files that direct Codex. Each one tells the agent how to approach a specific kind of task, so you stop restating the same rules in every prompt.
MCP, the Model Context Protocol, has been described as a USB port for language models. Here it connects Codex to Figma so the agent can read the design directly.
Codex is the agent that does the building. Codex is the harness, the user interface you type into. The model underneath is a separate thing, and other harnesses can run on the same OpenAI models.
Skills take the most work and carry the build. MCP is optional. If you do not use Figma, ask ChatGPT for a wireframe of a contact app and paste the HTML into Codex in place of the Figma links in the prompts below.
Why Write Skills
A skill formalizes the prompting techniques you have picked up over the last couple of years and installs them into the agent, so the agent has detailed instructions for the exact kind of work you are asking for. Three things make that worth the effort:
Keep it in line: specific skills for specific tasks keep the agent aligned and focused on the objective, and across several developers and projects everyone ends up doing the same thing the same way.
Cold starts: on a fresh codebase there are no local patterns to copy, so the skill supplies them.
Enforce rules: standard guardrails apply automatically. You could add a security skill or a personal-information skill the same way, without restating them in every prompt.
How to Prompt Effectively
The prompts in this build are short because the skills carry the detail. Four habits keep them effective:
Context rich: declare folders, code references, requirements, and limits up front. Here that means pointing Codex at the default app it will transform.
Use skills: custom, project-tailored skills keep the agent on the rails.
MCP servers inject secure runtime context such as designs, schemas, and other systems. In this build that context is the Figma components.
Right model: light for fast tasks, standard for daily work, heavy for complex logic. This app was built on GPT-5.6 Terra, the medium option, and an earlier or lighter model would handle something this simple.
The Design in Figma
The app has two screens, and you draw both in a new Figma file before you touch Codex. The first is the contact list: a column of rows, each with a round avatar icon and a name, and a final row with a green plus icon and the words Add account. The second is the add form: a First Name field, a Last Name field, and a green Submit button. Put each screen in its own frame, then turn the contact row, the Add account row, and the three form controls into components, since the prompts later link one node for each. The whole file takes about five minutes.
The screenshot above is the finished file. The skills, the prompts, and the completed app are in the repository at https://github.com/godfreynolan/skills, so clone it to follow along. The repository also carries an export of this design as AI Example.fig at the root, which you can import into Figma if you would rather not draw it. Either way the file lives in your account, and nothing in the rest of the tutorial depends on Figma specifically, only on having a design the agent can read.
The Toolchain
The app is written in Kotlin Multiplatform, so one Kotlin codebase produces a desktop app and a web app, and the same code could target Android and iOS. The IDE is IntelliJ IDEA with the Kotlin Multiplatform plugin. Install IntelliJ, open Settings, then Plugins, search the Marketplace for Kotlin Multiplatform, and install it.
None of this is required by Codex. If you would rather build the same app in Python and HTML in VS Code, every step below works the same way with different file names.
Create and Run the Default Project
It helps if Codex has something to start from, so generate the stock Kotlin Multiplatform project first. Choose File, New Project, pick Kotlin Multiplatform from the generators list, and name it contact_app to match the repository, leaving the Project ID as com.example.contact_app. The screenshot below uses demo, and every path later in this tutorial assumes contact_app.
Check Create Git repository, since you will want to commit after every prompt. Check Desktop and Web with Share UI selected for Web, leave Android, iOS, and Server unchecked, and keep Include tests checked, since the testing skill will need a test framework later.
Run the web target to make sure everything works before handing it to an agent. Every Gradle command in this tutorial runs from the project root in a terminal, and on Windows the wrapper is gradlew.bat:
./gradlew :webApp:wasmJsBrowserDevelopmentRun
./gradlew :webApp:wasmJsBrowserDevelopmentRun
./gradlew :webApp:wasmJsBrowserDevelopmentRun
The first build downloads a great deal and takes several minutes. When it finishes, the app is served on port 8080 and shows a Click me button and the Compose Multiplatform logo.
The desktop target runs with the matching Gradle task:
./gradlew :desktopApp:run
./gradlew :desktopApp:run
./gradlew :desktopApp:run
A New Project in Codex
Install the ChatGPT desktop app, which has Windows, Mac, and Linux versions. Codex lives inside it, and the screenshots in this tutorial show the app with Work selected in the dropdown at the top left of the sidebar. Choose New chat, then Create project, give it a name, and under Source folders add the folder containing the default app you just generated.
Codex takes a few seconds to index the folder, and every later prompt runs inside this project.
Install the Figma Plugin and Verify Access
Open Plugins in the left sidebar, search for figma, and click Install. There are hundreds of plugins, including GitHub, databases, and Google Drive, and this is the same screen you would use for any of them.
Next find the link for a component. Open your file in Figma in Dev Mode, which adds m=dev to the end of every link, select the contact row on the canvas, open the Inspect tab on the right, and expand the MCP panel. It shows a ready-made prompt with the node link, and Copy example prompt puts it on the clipboard.
Paste the link into Codex and ask it to confirm the connection works:
verify that you can use this figma link
@<https://www.figma.com/design/><your-file-id>/<your-file-name>?node-id=<your-node>&m=dev
verify that you can use this figma link
@<https://www.figma.com/design/><your-file-id>/<your-file-name>?node-id=<your-node>&m=dev
verify that you can use this figma link
@<https://www.figma.com/design/><your-file-id>/<your-file-name>?node-id=<your-node>&m=dev
Codex reports that it can access the file and the node, and describes what it resolved to. If that check fails, nothing later will work, so fix it now.
The prompt files in the repository carry the author's Figma URLs, which point at a private file, so the prompts in this tutorial show a placeholder wherever a link goes. You will need six links from your own file: the contact row, the Add account row, the list page frame, the First Name field, the Last Name field, and the Submit button. Copy each one the same way, by selecting it in Dev Mode and using the MCP panel.
Anatomy of a Skill
A skill is a folder named for the task, holding a file called SKILL.md. Inside are workflow instructions, guidelines, guardrails so the agent does not do anything strange, and examples. Install it once and call it from any prompt by typing a dollar sign and the skill name.
Here is the top of the component skill from the repository, in skills/kotlin-multiplatform-components/SKILL.md:
---
name: kotlin-multiplatform-components
description: Create or refactor Kotlin Multiplatform UI components that are stateless, theme-driven, reusable, responsive, and composition-first. Use when building shared Compose components, breaking down existing UI into reusable parts, or reviewing component code for KMP design-system compliance.
---
# Kotlin Multiplatform Components
## Purpose
Use this skill when a user wants a Kotlin Multiplatform component built, refactored, or reviewed for reuse.
## Core Rules
1.Keep components stateless. Do not store screen or business state inside the component.
1.Use only theme-provided colors, typography, spacing, shapes, and elevation. Never hard code colors.
1.Prefer composition. Break complex UI into smaller reusable components.
1.Reuse existing components first. Only create a new component when no suitable one exists.
1.Keep business logic out of components. Limit code to UI and presentation logic.
1.Make components responsive and layout-aware. Constrain size from the parent layout instead of fixed dimensions when possible.
1.If Figma is provided, treat it as the visual reference and match structure, hierarchy, and spacing as closely as the codebase allows.
---
name: kotlin-multiplatform-components
description: Create or refactor Kotlin Multiplatform UI components that are stateless, theme-driven, reusable, responsive, and composition-first. Use when building shared Compose components, breaking down existing UI into reusable parts, or reviewing component code for KMP design-system compliance.
---
# Kotlin Multiplatform Components
## Purpose
Use this skill when a user wants a Kotlin Multiplatform component built, refactored, or reviewed for reuse.
## Core Rules
1.Keep components stateless. Do not store screen or business state inside the component.
1.Use only theme-provided colors, typography, spacing, shapes, and elevation. Never hard code colors.
1.Prefer composition. Break complex UI into smaller reusable components.
1.Reuse existing components first. Only create a new component when no suitable one exists.
1.Keep business logic out of components. Limit code to UI and presentation logic.
1.Make components responsive and layout-aware. Constrain size from the parent layout instead of fixed dimensions when possible.
1.If Figma is provided, treat it as the visual reference and match structure, hierarchy, and spacing as closely as the codebase allows.
---
name: kotlin-multiplatform-components
description: Create or refactor Kotlin Multiplatform UI components that are stateless, theme-driven, reusable, responsive, and composition-first. Use when building shared Compose components, breaking down existing UI into reusable parts, or reviewing component code for KMP design-system compliance.
---
# Kotlin Multiplatform Components
## Purpose
Use this skill when a user wants a Kotlin Multiplatform component built, refactored, or reviewed for reuse.
## Core Rules
1.Keep components stateless. Do not store screen or business state inside the component.
1.Use only theme-provided colors, typography, spacing, shapes, and elevation. Never hard code colors.
1.Prefer composition. Break complex UI into smaller reusable components.
1.Reuse existing components first. Only create a new component when no suitable one exists.
1.Keep business logic out of components. Limit code to UI and presentation logic.
1.Make components responsive and layout-aware. Constrain size from the parent layout instead of fixed dimensions when possible.
1.If Figma is provided, treat it as the visual reference and match structure, hierarchy, and spacing as closely as the codebase allows.
The frontmatter name is what you type after the dollar sign, and the description tells the agent when the skill applies. Below that, the structure is purpose, core rules, an implementation checklist, and output expectations. The skill folder can also hold a references subfolder with longer guidance and an agents/openai.yaml file that gives the skill a display name and a default prompt in the Codex UI.
The fastest way to write one is to describe the rules you want in a sentence and ask ChatGPT to turn them into a skill. The component skill started as “create or refactor Kotlin Multiplatform UI components that are stateless, theme driven, reusable, responsive, and composition first,” and ChatGPT broke that down into the rules above. Asking it what a good SKILL.md should and should not contain is a useful second question.
The Skills on the Shelf
Four skills drive this build, each paired with a prompt file in the repository’s prompts folder:
Skill
Prompt
What it encodes
kotlin-multiplatform-components
ComponentPrompt1, ComponentPrompt2
Reusable, stateless Compose components with previews
feature-implementation
ImplementationPrompt
MVVM features with repositories, coroutines, and data boundaries
unit-testing
TestingPrompt
Model and ViewModel tests with mocks and coverage targets
cleanup-enforcer
CleanupPrompt
Architecture audit, dead code removal, code-smell fixes
The feature skill is the one that turns components into an application. Its workflow section is a nine-step order of operations, and its architecture rules are the guardrails:
## Workflow
1.Inspect the UI component library first.
2.Reuse existing components whenever possible.
3.If a required UI component does not exist, create it with the component skill before wiring the feature.
4.Build the View layer and keep it focused on rendering and user events.
5.Build the ViewModel as the bridge between View and Model.
6.Build the Model layer with data models, repositories, and business logic.
7.Connect the layers with explicit, testable interfaces.
8.Match the visual design to the provided Figma spec as closely as possible.
9.Use existing theme colors and typography tokens first; if a required token does not exist, create it in the theme layer before implementing the UI.
## Architecture Rules
-Always follow MVVM.
-Keep the View stateless where possible and state-driven by the ViewModel.
-Put presentation state and UI event handling in the ViewModel.
-Keep business logic out of the View and out of reusable UI components.
-Keep data access behind repositories.
-Prefer dependency injection over concrete coupling.
-Keep each class or module focused on one responsibility.
## Workflow
1.Inspect the UI component library first.
2.Reuse existing components whenever possible.
3.If a required UI component does not exist, create it with the component skill before wiring the feature.
4.Build the View layer and keep it focused on rendering and user events.
5.Build the ViewModel as the bridge between View and Model.
6.Build the Model layer with data models, repositories, and business logic.
7.Connect the layers with explicit, testable interfaces.
8.Match the visual design to the provided Figma spec as closely as possible.
9.Use existing theme colors and typography tokens first; if a required token does not exist, create it in the theme layer before implementing the UI.
## Architecture Rules
-Always follow MVVM.
-Keep the View stateless where possible and state-driven by the ViewModel.
-Put presentation state and UI event handling in the ViewModel.
-Keep business logic out of the View and out of reusable UI components.
-Keep data access behind repositories.
-Prefer dependency injection over concrete coupling.
-Keep each class or module focused on one responsibility.
## Workflow
1.Inspect the UI component library first.
2.Reuse existing components whenever possible.
3.If a required UI component does not exist, create it with the component skill before wiring the feature.
4.Build the View layer and keep it focused on rendering and user events.
5.Build the ViewModel as the bridge between View and Model.
6.Build the Model layer with data models, repositories, and business logic.
7.Connect the layers with explicit, testable interfaces.
8.Match the visual design to the provided Figma spec as closely as possible.
9.Use existing theme colors and typography tokens first; if a required token does not exist, create it in the theme layer before implementing the UI.
## Architecture Rules
-Always follow MVVM.
-Keep the View stateless where possible and state-driven by the ViewModel.
-Put presentation state and UI event handling in the ViewModel.
-Keep business logic out of the View and out of reusable UI components.
-Keep data access behind repositories.
-Prefer dependency injection over concrete coupling.
-Keep each class or module focused on one responsibility.
MVVM is Model, View, ViewModel, an architecture pattern that keeps the screen, the screen’s state, and the data behind it in separate layers. The same file goes on to require SQLDelight for local persistence, coroutines on Dispatchers.IO for database work, and a code-smell check against the Refactoring.Guru catalog, which one of the developers on the team called out by name.
Install and Verify the Skills
Copy the skills folder from the repository into your project. The first prompt tells Codex what you are building and asks it to install the skills, from prompts/SetupPrompts.md:
We're going to create a kotlin multiplatform application,
that targets desktop, and the web. The idea of the application is
to create a contacts page that allows the user to view and add contacts
to a list.
Before we begin I've created several skills under contact_app/skills that I need
you to install. Please install the kotlin-multiplatform-components, feature-implementation, and unit testing skills.
We're going to create a kotlin multiplatform application,
that targets desktop, and the web. The idea of the application is
to create a contacts page that allows the user to view and add contacts
to a list.
Before we begin I've created several skills under contact_app/skills that I need
you to install. Please install the kotlin-multiplatform-components, feature-implementation, and unit testing skills.
We're going to create a kotlin multiplatform application,
that targets desktop, and the web. The idea of the application is
to create a contacts page that allows the user to view and add contacts
to a list.
Before we begin I've created several skills under contact_app/skills that I need
you to install. Please install the kotlin-multiplatform-components, feature-implementation, and unit testing skills.
Codex works for under a minute and replies with the list of skills it installed and verified. They become available from the next message.
Open Plugins and switch to the Skills tab to see the installed list, or type a dollar sign in the prompt box and the matching skills appear as you type.
Six Prompts, One App
Six prompts produce the whole app, and they run in this order:
The prompt files in the repository show each skill mention as a link to a path on the author's machine, because that is how the app renders it. The prompts here show what you type instead: a dollar sign and the skill name, picked from the autocomplete.
Each prompt is one focused ask. Components and features never share a prompt, and when something comes out wrong the corrective follow-up stays narrow instead of reopening the whole task. Commit after each prompt, since Codex will sometimes replace code you liked and git is how you get it back.
Components First: ContactListItem
The first visible result is the contact row. In your Figma file, select the row component, open Inspect, and copy the MCP link from the panel on the right.
The prompt names the skill, pastes your link in place of the author's, and states the parameters and the two requirements, from prompts/ComponentPrompt1.md:
Utilizing the $kotlin-multiplatform-components skill implement this design from Figma.
Implement this design from Figma.
@<yourContactListItemnodelink>
Create a new component called ContactListItem should accept parameters for a users first name and last name and a method for onClick.
Ensure you create a preview for this component and the design matches figma exactly.
Utilizing the $kotlin-multiplatform-components skill implement this design from Figma.
Implement this design from Figma.
@<yourContactListItemnodelink>
Create a new component called ContactListItem should accept parameters for a users first name and last name and a method for onClick.
Ensure you create a preview for this component and the design matches figma exactly.
Utilizing the $kotlin-multiplatform-components skill implement this design from Figma.
Implement this design from Figma.
@<yourContactListItemnodelink>
Create a new component called ContactListItem should accept parameters for a users first name and last name and a method for onClick.
Ensure you create a preview for this component and the design matches figma exactly.
Codex reads the skill, pulls the design context from Figma, and about two and a half minutes later reports a new ContactListItem.kt and an exported person icon SVG, with JVM, JS, and Wasm compilation verified.
The public composable in shared/src/commonMain/kotlin/com/example/contact_app/components/ContactListItem.kt is this:
The three parameters the prompt asked for are there, plus a trailing modifier so the parent can constrain the row, which is the skill’s responsive rule at work. The row fills its parent’s width, fixes its height at 72 dp to preserve the Figma spacing of a 40 dp avatar inset by 16 dp, and takes every color and text style from MaterialTheme rather than a hard-coded value. The avatar and the spacer are private composables in the same file, and the preview the prompt required is a @Preview at the bottom that renders the row with John Smith.
Same Again for the Button
The second component gets the same treatment with a shorter prompt, from prompts/ComponentPrompt2.md:
Utilize the $kotlin-multiplatform-components skill to create the AddAccountButton. Implement this design from Figma.
@<yourAddAccountButtonnodelink>
Utilize the $kotlin-multiplatform-components skill to create the AddAccountButton. Implement this design from Figma.
@<yourAddAccountButtonnodelink>
Utilize the $kotlin-multiplatform-components skill to create the AddAccountButton. Implement this design from Figma.
@<yourAddAccountButtonnodelink>
The result in AddAccountButton.kt mirrors the row, with one action that the screen owns:
The only input is onClick. The component does not know what adding an account means, which keeps it stateless and reusable. The green comes from the theme’s secondary color, and the plus icon sits in a circle filled with that same color at 12 percent opacity.
The Feature Prompt
The feature prompt connects the two components into an app, and it took the most work to get right. The version that worked is in prompts/ImplementationPrompt.md:
Utilizing the $feature-implementation skill I need you to implement the contact list from figma. The components for the contact list already exist utilize AddAccountButton, and ContactListItem for this feature. The contact list should be scrollable and display all the contacts in the database. You'll need to create a new table in the database called contacts that stores a list of the first name, last name, and UUID of each contact along with creating the repository to do basic CRUD operations on this entity. Query the database and display the list of contacts to the user. Implement this design from Figma for the contact list page.
@<yourcontactlistpagenodelink>
When the user clicks the add account button it will go to this page with these 3 components
@<yourFirstNamefieldnodelink>
@<yourLastNamefieldnodelink>
@<yourSubmitbuttonnodelink>
From there the user should be able to enter the users first name and last name into the text box and click submit. Error handling should happen on the fly if the user types in an invalid first or last name it will highlight in red and disable the submit button until a valid name is entered.
Don't hard code the names that are in the design this should be user driven and come from the sql database.
Utilizing the $feature-implementation skill I need you to implement the contact list from figma. The components for the contact list already exist utilize AddAccountButton, and ContactListItem for this feature. The contact list should be scrollable and display all the contacts in the database. You'll need to create a new table in the database called contacts that stores a list of the first name, last name, and UUID of each contact along with creating the repository to do basic CRUD operations on this entity. Query the database and display the list of contacts to the user. Implement this design from Figma for the contact list page.
@<yourcontactlistpagenodelink>
When the user clicks the add account button it will go to this page with these 3 components
@<yourFirstNamefieldnodelink>
@<yourLastNamefieldnodelink>
@<yourSubmitbuttonnodelink>
From there the user should be able to enter the users first name and last name into the text box and click submit. Error handling should happen on the fly if the user types in an invalid first or last name it will highlight in red and disable the submit button until a valid name is entered.
Don't hard code the names that are in the design this should be user driven and come from the sql database.
Utilizing the $feature-implementation skill I need you to implement the contact list from figma. The components for the contact list already exist utilize AddAccountButton, and ContactListItem for this feature. The contact list should be scrollable and display all the contacts in the database. You'll need to create a new table in the database called contacts that stores a list of the first name, last name, and UUID of each contact along with creating the repository to do basic CRUD operations on this entity. Query the database and display the list of contacts to the user. Implement this design from Figma for the contact list page.
@<yourcontactlistpagenodelink>
When the user clicks the add account button it will go to this page with these 3 components
@<yourFirstNamefieldnodelink>
@<yourLastNamefieldnodelink>
@<yourSubmitbuttonnodelink>
From there the user should be able to enter the users first name and last name into the text box and click submit. Error handling should happen on the fly if the user types in an invalid first or last name it will highlight in red and disable the submit button until a valid name is entered.
Don't hard code the names that are in the design this should be user driven and come from the sql database.
Four things make this prompt work:
It calls the skill by name, so the MVVM workflow and architecture rules apply.
It links the exact Figma nodes for both screens, and the add screen gets a link per component.
It names what to reuse, ContactListItem and AddAccountButton, so the agent does not rebuild them inline.
It demands database-driven data with a UUID per contact, so two John Smiths stay separate records and no names are hard-coded from the design. The shipped code only half delivers this, as described below.
The first attempt came back wobbly, and the fix was a narrow follow-up naming the first name, last name, and add button fields again rather than a rewrite of the prompt.
The Generated Layers
The skill’s MVVM rule shows up directly in the package layout. The domain layer in shared/src/commonMain/kotlin/com/example/contact_app/contacts/domain holds the entity and the repository contract:
The repository exposes the list as a StateFlow so the screen updates whenever the data changes, and the three mutating operations are suspend functions, which is how the skill keeps database work off the main thread.
The ViewModel in contacts/presentation/ContactViewModel.kt owns every piece of screen state, which screen is showing, the two text fields, and the validation rule:
The repository comes in through the constructor, which is the dependency injection the skill asked for and what makes the class testable with a mock. Each public value is a read-only view of a private MutableStateFlow.
The name pattern accepts a letter followed by up to 49 letters, spaces, apostrophes, or hyphens. The submit function trims both fields, refuses silently if either is invalid, creates the contact, clears the form, and returns to the list.
The view in App.kt switches between the two screens on the ViewModel’s state:
The list screen is a LazyColumn, which is what makes it scrollable, with one ContactListItem per contact keyed by UUID and the AddAccountButton as the last item. The add screen reads both fields from the ViewModel and disables Submit until both names pass. A field is marked as an error only once it is non-empty and invalid, which is the red highlight the prompt asked for.
The repository wired in App is InMemoryContactRepository, a class in contacts/data that keeps the list in a MutableStateFlow and generates each UUID with Uuid.random(). The git repository also contains a SQLDelight schema at shared/src/commonMain/sqldelight/com/example/contact_app/database/Contact.sq with the contacts table and selectAll, insertContact, updateContact, and deleteContact queries, which is what the prompt and the skill both asked for:
CREATETABLE contacts (
uuid TEXT NOTNULLPRIMARYKEY,
first_name TEXT NOTNULL,
last_name TEXT NOTNULL);
selectAll:SELECT * FROM contacts ORDERBY last_name, first_name;
insertContact:INSERTINTO contacts(uuid, first_name, last_name)VALUES(?, ?, ?);
updateContact:UPDATE contacts SET first_name = ?, last_name = ? WHERE uuid = ?;
deleteContact:DELETEFROM contacts WHERE uuid = ?;
CREATETABLE contacts (
uuid TEXT NOTNULLPRIMARYKEY,
first_name TEXT NOTNULL,
last_name TEXT NOTNULL);
selectAll:SELECT * FROM contacts ORDERBY last_name, first_name;
insertContact:INSERTINTO contacts(uuid, first_name, last_name)VALUES(?, ?, ?);
updateContact:UPDATE contacts SET first_name = ?, last_name = ? WHERE uuid = ?;
deleteContact:DELETEFROM contacts WHERE uuid = ?;
CREATETABLE contacts (
uuid TEXT NOTNULLPRIMARYKEY,
first_name TEXT NOTNULL,
last_name TEXT NOTNULL);
selectAll:SELECT * FROM contacts ORDERBY last_name, first_name;
insertContact:INSERTINTO contacts(uuid, first_name, last_name)VALUES(?, ?, ?);
updateContact:UPDATE contacts SET first_name = ?, last_name = ? WHERE uuid = ?;
deleteContact:DELETEFROM contacts WHERE uuid = ?;
The shared module’s build file does not apply the SQLDelight plugin, so that schema is never compiled and the shipped app loses its contacts when it closes. Persistence is out of scope for this tutorial, and the fix is a seventh prompt to the feature skill asking for the SQLDelight Gradle plugin and a repository implementation that uses the generated queries behind the same ContactRepository interface. The ViewModel and the screens will not need to change, because the skill’s architecture rules keep them dependent only on the ContactRepository interface.
A Working App
Run the desktop target and you get the contact list with one Add account row. Click it, type a first and last name, and Submit returns you to the list with the new contact in place.
The web target serves the same screens on port 8080.
The Unit-Testing Skill
You did not write this code and have not read it, so the unit-testing skill supplies some insurance that it does what the prompt said. The prompt is one sentence, from prompts/TestingPrompt.md:
Utilize the [$unit-testing](/Users/admin/.codex/skills/unit-testing/SKILL.md) skill to write unit tests for this application ensure we get 100% code coverage on the model and view model layers.
Utilize the [$unit-testing](/Users/admin/.codex/skills/unit-testing/SKILL.md) skill to write unit tests for this application ensure we get 100% code coverage on the model and view model layers.
Utilize the [$unit-testing](/Users/admin/.codex/skills/unit-testing/SKILL.md) skill to write unit tests for this application ensure we get 100% code coverage on the model and view model layers.
The skill fixes the framework at JUnit with Mockito for mocks, requires that anything external to the unit under test is mocked, and names tests in the form givenCondition_whenThisOccurs_thenExpectedResult. The generated tests land in shared/src/jvmTest/kotlin/com/example/contact_app/contacts/ContactFeatureTest.kt. The ViewModel tests mock the repository and check each state transition:
The helper stubs the repository’s contacts flow so the ViewModel can be constructed against a mock. The submit test pads both names with spaces, submits, and then verifies the repository received the trimmed values, the fields were cleared, and the screen returned to the list. Other tests cover the initial state, navigation, the boundary cases of the name pattern, the invalid-submit path that must never call create, and the in-memory repository’s sorted create, update, and delete behavior.
Run the JVM tests from the command line:
./gradlew :shared:jvmTest
./gradlew :shared:jvmTest
./gradlew :shared:jvmTest
Gradle writes an HTML report to shared/build/reports/tests/jvmTest/index.html showing nine tests, zero failures, and 100 percent success. Seven of those are the feature tests above, and the other two are the starter project’s placeholder tests, which only assert that one plus two is three.
Open the test file and read it. The report only tells you the tests pass, not that they test anything realistic, and checking that is part of the review job.
The Cleanup Skill
Codex creates a great deal of code, and not all of it is needed. The final skill audits the repository and makes it manageable for the next developer. The prompt is one line, from prompts/CleanupPrompt.md:
Utilize the new $cleanup-enforcer skill to clean up the code in this repository
Utilize the new $cleanup-enforcer skill to clean up the code in this repository
Utilize the new $cleanup-enforcer skill to clean up the code in this repository
The setup prompt installed only three skills, so Codex installs this one from the skills folder as part of the run and says so in its report. The skill tells the agent to map the architecture before editing, to use the project’s documented conventions or an MVVM baseline, and to work through all 23 Refactoring.Guru code smells, marking each as fixed, not found, not applicable, or deferred. It is also told to run whatever formatter, linter, and tests the project provides and to say so if a gate was unavailable rather than call the code clean.
In this build it split mixed responsibilities into domain, data, and presentation packages, removed the unused Greeting code left over from the starter project, reformatted the app UI, and reran the JVM tests to confirm BUILD SUCCESSFUL. It reported that duplication, dead code, divergent change, long methods, and inappropriate coupling were fixed, that the other 18 smell categories did not apply to a codebase this small, and that no linter was configured, so lint-clean status stayed unverified. The first run of the app had shown a stray starter element above the contact list, and cleanup removed it, leaving the app shown in the title image.
What Carried the Build
Four things made this work, and they apply to any project, not only this one:
Front-load the context: the prompt is the spec, so folders, references, requirements, and limits go in before the ask.
One focused prompt per component or feature, never mixed, with corrective follow-ups kept narrow.
Expect imperfection: it will not be right the first time, so review the output, correct the specific thing that is wrong, and feed better prompts and examples back into the skills.
Cleanup and tests are repeatable quality gates; run them after every feature, since the agent will also run the tests to check its own refactors.
A slide from an internal show-and-tell at the office made the same point, that the AI will not always get results perfect on the first try and code reviews are essential. For a hello-world contact app that review is a formality, but for anything you ship it is the job that remains after the typing goes away.
Conclusion
You now have a contact list app running on desktop and in the browser, built by Codex from a Figma design you drew yourself, without writing a line of code. Alongside it you have four skills that encode how components, features, tests, and cleanup should be done, six prompts that replay the build in order, and seven passing unit tests that prove the ViewModel and repository behave. Codex still needs you to review what it produces and correct it narrowly, by design. We can’t wait to see what you build!