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_addcommand is now accessed viaformlm_exec. Theformlm_generatesmart 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 usingformlm_execfor manual field operations.
What Happened
Here’s the sequence:
- I asked Claude to build a customer satisfaction survey with 12 fields
- Claude created the app, added 8 fields, and then the session timed out
- I started a new session and said “continue building the customer satisfaction survey”
- Claude called
app_list, found the existing app, and started adding fields again - 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:
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.
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.
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:
- Check-first: Provide a read operation that lets the AI verify whether the resource already exists
- Document it: In the schema description, explicitly say “Check with [X] before calling this to avoid duplicates”
- 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:
- Call
field_listto see all 20 fields - Identify the duplicates (fields with suffixed IDs like
email_1,email_2) - Call
field_removefor each duplicate - 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.
답글 남기기