DEV Community

Keo Fung | FormLM
Keo Fung | FormLM

Posted on

When Your AI Duplicates Every Field: A Story About Idempotency

I had a form with 12 fields. Then my Claude Desktop session crashed. I restarted it, asked Claude to "continue building the form," and ended up with 24 fields. Half of them were exact duplicates — same IDs, same titles, same types. The form rendered with two "Email" fields, two "Name" fields, and two of every radio button.

This is the story of how field_add isn't idempotent, why that matters when an AI is driving, and the pattern I adopted to fix it.

Update (v0.2.0): The field_add command is now accessed via formlm_exec. The formlm_generate smart pipeline eliminates this entire class of problem — the server-side AssessAgent handles field creation atomically and idempotently. The find-before-add pattern remains useful when using formlm_exec for manual field operations.

What Happened

Here's the sequence:

  1. I asked Claude to build a customer satisfaction survey with 12 fields
  2. Claude created the app, added 8 fields, and then the session timed out
  3. I started a new session and said "continue building the customer satisfaction survey"
  4. Claude called app_list, found the existing app, and started adding fields again
  5. It re-added all 12 fields — including the 8 that were already there

The result was a form with 20 fields total (8 original + 12 duplicate). The duplicate field IDs were auto-suffixed by the server (email became email, email_1, email_2...). To the user filling out the form, it looked like the form had been designed by someone who really, really wanted your email address.

Why field_add Isn't Idempotent

Here's the thing about field_add — it's a create operation, not an upsert. If you call it twice with the same field ID, the server creates two fields. The field ID isn't a unique constraint; it's a label.

Looking at the MCP tool definition:

server.tool('field_add', 'Add a new field', {
  appId: z.string().describe('App ID'),
  id: z.string().describe('Field ID'),
  type: z.string().default('input').describe('Field type (default: input)'),
  title: z.string().optional().describe('Field title / question text'),
  // ...
}, async (params) => {
  let cmd = `assess form add --app ${params.appId} --id ${params.id} --type ${params.type}`;
  // ...
});
Enter fullscreen mode Exit fullscreen mode

There's no check for "does this field ID already exist?" The tool just sends the add command and the server creates a new field. If a field with that ID already exists, the server either appends a suffix or creates a duplicate.

From a CLI perspective, this is fine. A human running formlm-cli field add knows whether they've already added a field. They don't need the CLI to check for them.

From an AI perspective, this is a problem. The AI doesn't remember what it did in a previous session. It doesn't check whether fields exist before adding them. It just executes the plan, and if the plan says "add 12 fields," it adds 12 fields — regardless of whether 8 of them are already there.

The Fix: Find Before Add

The pattern I now enforce is: always check before adding. Before calling field_add, call field_find to see if a field with that ID already exists:

# Step 1: Check if the field already exists
field_find --app <appId> --id email

# If the response is "field not found", proceed with add:
field_add --app <appId> --id email --type input --title "Email" --required

# If the field already exists, skip the add (or update it instead)
Enter fullscreen mode Exit fullscreen mode

In practice, I instruct Claude to batch this: call field_list once to get all existing fields, then only add the ones that are missing.

# Get the current state
field_list --app <appId>
# Response: [name, email, phone, rating]

# Claude now knows which fields exist and only adds the missing ones
# It needs to add: comments, feedback_source
field_add --app <appId> --id comments --type textarea --title "Additional comments"
field_add --app <appId> --id feedback_source --type radio --options "Search:1,Social:2,Friend:3"
Enter fullscreen mode Exit fullscreen mode

This is the read-before-write pattern again (same as find-then-set from the previous article), but applied to creation instead of updates.

Why Not Just Make field_add Idempotent?

I considered making field_add an upsert — if the field exists, update it; if not, create it. But that has its own problems:

  1. Silent overwrites. If the AI accidentally re-adds a field with different options, the upsert would silently overwrite the existing field's options. No error, no warning — just changed data.

  2. No explicit "create" semantics. Sometimes you want to fail if a field already exists — like when you're setting up a new form and a duplicate field ID indicates a bug in your plan.

  3. The update path is different. Creating a field and updating a field go through different server-side validation. Merging them into one operation muddies the API.

So I kept field_add as a pure create operation and moved the idempotency check to the caller. The AI is responsible for checking before adding.

The Idempotency Wrapper

In my system prompt for Claude, I now include this instruction:

"Before adding any field, call field_list to get the current field set. Only add fields whose IDs don't already exist in the form. If a field already exists and needs changes, use field_update or field_set_property instead of field_add."

This is a soft constraint — it relies on Claude reading and following the instruction. In practice, it works about 90% of the time. The other 10%, Claude skips the field_list call and goes straight to field_add, usually when it's "in a hurry" (i.e., the context window is getting full and it's trying to save tokens).

The hard constraint would be making the server reject duplicate field IDs. But as I mentioned, field IDs aren't unique constraints — they're labels. Making them unique would break existing forms that intentionally use the same ID across different field types (don't ask).

The Deeper Lesson: AI Agents Need Idempotency Everywhere

This isn't just about field_add. Every create operation in an AI-accessible API needs an idempotency story:

  • app_create — what if the AI creates the same app twice? Currently it creates two apps with the same name. I should probably check for existing apps with the same name before creating.
  • share_publish — what if the AI publishes the same form twice? Currently it's idempotent (publishing an already-published form is a no-op). This one got it right.
  • auth_login — what if the AI logs in twice? Currently it overwrites the existing token. Also fine.

The pattern I now follow for any create operation:

  1. Check-first: Provide a read operation that lets the AI verify whether the resource already exists
  2. Document it: In the schema description, explicitly say "Check with [X] before calling this to avoid duplicates"
  3. Soft idempotency: If the operation is called with the same parameters, make it a no-op rather than creating a duplicate, where possible

The Cleanup

Cleaning up the duplicated form was painful. I had to:

  1. Call field_list to see all 20 fields
  2. Identify the duplicates (fields with suffixed IDs like email_1, email_2)
  3. Call field_remove for each duplicate
  4. Re-order the remaining fields with field_move

Twelve field_remove calls. Each one was a round-trip to the server. It took longer to clean up than it took to create the mess.

After that experience, I wrote a cleanup script that detects and removes duplicate fields by comparing titles and types. It's not part of the CLI — it's a one-off shell script I keep in my snippets folder. If this happens often enough, I might add a field_dedup command to the CLI. But for now, the find-before-add pattern is enough.


The formlm-cli MCP server documents the find-before-add pattern in tool descriptions. Open source at github.com/formlm/cli — try the platform at formlm.me.

Top comments (0)