{"slug": "when-your-ai-duplicates-every-field-a-story-about-idempotency", "title": "When Your AI Duplicates Every Field: A Story About Idempotency", "summary": "A developer building a form with Claude Desktop encountered a bug where the AI duplicated every field after a session crash, resulting in 24 fields instead of 12. The root cause is that the `field_add` command is not idempotent, so the AI re-added fields without checking for existing ones. The developer fixed it by enforcing a read-before-write pattern: always check if a field exists before adding it.", "body_md": "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.\n\nThis is the story of how `field_add`\n\nisn't idempotent, why that matters when an AI is driving, and the pattern I adopted to fix it.\n\nUpdate (v0.2.0): The`field_add`\n\ncommand is now accessed via`formlm_exec`\n\n. The`formlm_generate`\n\nsmart 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`\n\nfor manual field operations.\n\nHere's the sequence:\n\n`app_list`\n\n, found the existing app, and started adding fields againThe result was a form with 20 fields total (8 original + 12 duplicate). The duplicate field IDs were auto-suffixed by the server (`email`\n\nbecame `email`\n\n, `email_1`\n\n, `email_2`\n\n...). To the user filling out the form, it looked like the form had been designed by someone who really, really wanted your email address.\n\n`field_add`\n\nIsn't Idempotent\nHere's the thing about `field_add`\n\n— 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.\n\nLooking at the MCP tool definition:\n\n```\nserver.tool('field_add', 'Add a new field', {\n  appId: z.string().describe('App ID'),\n  id: z.string().describe('Field ID'),\n  type: z.string().default('input').describe('Field type (default: input)'),\n  title: z.string().optional().describe('Field title / question text'),\n  // ...\n}, async (params) => {\n  let cmd = `assess form add --app ${params.appId} --id ${params.id} --type ${params.type}`;\n  // ...\n});\n```\n\nThere'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.\n\nFrom a CLI perspective, this is fine. A human running `formlm-cli field add`\n\nknows whether they've already added a field. They don't need the CLI to check for them.\n\nFrom 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.\n\nThe pattern I now enforce is: **always check before adding**. Before calling `field_add`\n\n, call `field_find`\n\nto see if a field with that ID already exists:\n\n```\n# Step 1: Check if the field already exists\nfield_find --app <appId> --id email\n\n# If the response is \"field not found\", proceed with add:\nfield_add --app <appId> --id email --type input --title \"Email\" --required\n\n# If the field already exists, skip the add (or update it instead)\n```\n\nIn practice, I instruct Claude to batch this: call `field_list`\n\nonce to get all existing fields, then only add the ones that are missing.\n\n```\n# Get the current state\nfield_list --app <appId>\n# Response: [name, email, phone, rating]\n\n# Claude now knows which fields exist and only adds the missing ones\n# It needs to add: comments, feedback_source\nfield_add --app <appId> --id comments --type textarea --title \"Additional comments\"\nfield_add --app <appId> --id feedback_source --type radio --options \"Search:1,Social:2,Friend:3\"\n```\n\nThis is the read-before-write pattern again (same as find-then-set from the previous article), but applied to creation instead of updates.\n\n`field_add`\n\nIdempotent?\nI considered making `field_add`\n\nan upsert — if the field exists, update it; if not, create it. But that has its own problems:\n\n**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.\n\n**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.\n\n**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.\n\nSo I kept `field_add`\n\nas a pure create operation and moved the idempotency check to the caller. The AI is responsible for checking before adding.\n\nIn my system prompt for Claude, I now include this instruction:\n\n\"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.\"\n\nThis 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`\n\ncall and goes straight to `field_add`\n\n, usually when it's \"in a hurry\" (i.e., the context window is getting full and it's trying to save tokens).\n\nThe 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).\n\nThis isn't just about `field_add`\n\n. Every create operation in an AI-accessible API needs an idempotency story:\n\n`app_create`\n\n`share_publish`\n\n`auth_login`\n\nThe pattern I now follow for any create operation:\n\nCleaning up the duplicated form was painful. I had to:\n\n`field_list`\n\nto see all 20 fields`email_1`\n\n, `email_2`\n\n)`field_remove`\n\nfor each duplicate`field_move`\n\nTwelve `field_remove`\n\ncalls. Each one was a round-trip to the server. It took longer to clean up than it took to create the mess.\n\nAfter 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`\n\ncommand to the CLI. But for now, the find-before-add pattern is enough.\n\n*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.*", "url": "https://wpnews.pro/news/when-your-ai-duplicates-every-field-a-story-about-idempotency", "canonical_source": "https://dev.to/keofung/when-your-ai-duplicates-every-field-a-story-about-idempotency-3jj6", "published_at": "2026-07-24 08:55:00+00:00", "updated_at": "2026-07-24 09:06:36.358530+00:00", "lang": "en", "topics": ["developer-tools", "artificial-intelligence", "ai-agents"], "entities": ["Claude Desktop", "MCP"], "alternates": {"html": "https://wpnews.pro/news/when-your-ai-duplicates-every-field-a-story-about-idempotency", "markdown": "https://wpnews.pro/news/when-your-ai-duplicates-every-field-a-story-about-idempotency.md", "text": "https://wpnews.pro/news/when-your-ai-duplicates-every-field-a-story-about-idempotency.txt", "jsonld": "https://wpnews.pro/news/when-your-ai-duplicates-every-field-a-story-about-idempotency.jsonld"}}