It started with a radio field that lost its scores. Claude was trying to update one option's label on a question, and somehow every score on that field vanished. The form still worked — it just stopped scoring anything. Nobody noticed until a respondent completed the assessment and got a zero.
Here's how it happened, and the pattern I adopted to make sure it never happens again.
Update (v0.2.0): The individual field tools (
field_find,field_set_property,field_update) are now accessed viaformlm_exec— the direct command execution tool. The find-then-set pattern remains the recommended approach for fine-grained field updates. For bulk changes,formlm_modifywraps the server-side BuilderAgent pipeline which handles field updates safely.
The Incident
I had a radio field with three options:
field: q3
type: radio
options:
- "Satisfied:3"
- "Neutral:2"
- "Unsatisfied:1"
I asked Claude: "Change the 'Neutral' label to 'Okay' on field q3."
Claude called field_update:
field_update --app <appId> --id q3 --options "Satisfied:3,Okay:2,Unsatisfied:1"
Looks fine, right? It passed all three options, keeping the scores intact. The label changed. The scores stayed. Problem solved.
Except that's not what happened every time. In a different session, Claude decided to be "efficient" and only pass the option it wanted to change:
field_update --app <appId> --id q3 --options "Okay:2"
The --options parameter in field_update replaces the entire options list. It doesn't merge. So that command wiped out "Satisfied:3" and "Unsatisfied:1" and left the field with a single option: "Okay:2."
The form now had one radio button instead of three. The other two options were gone. And since the remaining option had a score of 2, every respondent who clicked it got 2 points — for a question that used to have a range of 1 to 3.
The respondent who triggered the discovery answered "Okay" on this question and nothing else — because there was nothing else to answer. They got a 2 where they should've gotten context-dependent scoring.
Why This Happened
The field_update command is documented as "Update a field." From an AI's perspective, that's a reasonable tool to use when you want to change something about a field. The schema says:
options: z.string().optional().describe('Options list — replaces existing options')
The description says "replaces existing options." That's accurate. But AI agents don't always read carefully when they're in the middle of a task. They see "update field" and "options" and they pass the option they want to change. The "replaces" part gets lost in the context window.
This isn't Claude's fault. It's a UX problem with the tool design. field_update is a blunt instrument — it replaces whatever you pass. If you only pass --options, you get a new options list. If you only pass --title, you get a new title and everything else stays. But the options parameter is particularly dangerous because it's a list — partial replacement of a list silently destroys data.
The Fix: find-then-set
I added field_set_property specifically for this scenario. Instead of replacing the whole field, it updates a single property at a precise path:
server.tool('field_set_property', 'Fine-grained field property update', {
appId: z.string().describe('App ID'),
id: z.string().optional().describe('Field ID'),
key: z.string().optional().describe('Field Key'),
property: z.string().describe('Property path (e.g. options.1.score)'),
value: z.string().describe('Property value'),
}, async (params) => {
let cmd = `assess form set-property --app ${params.appId}`;
if (params.id) cmd += ` --id ${params.id}`;
if (params.key) cmd += ` --key ${params.key}`;
cmd += ` --property ${params.property} --value "${params.value}"`;
const r = await execCommand(cmd);
// ...
});
Now, to change the "Neutral" label without touching scores, the AI uses:
field_set_property --app <appId> --id q3 --property options.1.label --value "Okay"
This updates exactly one property — the label of option index 1. Nothing else changes. The scores, the other options, the field type — all untouched.
But there's a catch. The AI needs to know that option index 1 is "Neutral" before it can update it. That's where field_find comes in.
The Full Pattern
The pattern I now enforce in my prompts is: find first, then set property.
- Call
field_findto get the current field state - Identify the exact property path that needs changing
- Call
field_set_propertywith that precise path
# Step 1: Find the field
field_find --app <appId> --id q3
# Response includes:
# options: [
# { label: "Satisfied", score: 3 },
# { label: "Neutral", score: 2 },
# { label: "Unsatisfied", score: 1 }
# ]
# Step 2: I want to change options[1].label
# Step 3: Set the property
field_set_property --app <appId> --id q3 --property options.1.label --value "Okay"
This is more API calls (find + set vs. a single update). But it's safe. The AI can't accidentally destroy data because it's only touching one property at a time, and it saw the current state before making the change.
When to Use update vs. set-property
I still use field_update for legitimate full replacements — when I actually want to replace the entire options list, or change the field type, or update multiple properties at once. The rule I follow:
-
Use
field_updatewhen you're replacing the entire structure of a field (new type, new options list, new title — a deliberate full replacement) -
Use
field_set_propertywhen you're tweaking one thing (one option's label, one option's score, one property)
The AI needs to be told this explicitly. In my system prompt, I now include: "When changing a single property of a field, always use field_set_property with a property path. Use field_update only for full field replacement."
That instruction, combined with the find-first pattern, has eliminated the "disappearing options" bug entirely. The cost is one extra API call per update. The benefit is not silently destroying form data.
The Bigger Lesson
When you design tools for AI agents, you have to think about what happens when the AI takes you literally but incompletely. "Update the label" doesn't mean "replace the entire options list with just the new label." But if the only tool available is a blunt "update" command, that's what can happen.
Fine-grained tools (set-property) combined with a read-before-write pattern (find before set) make AI interactions safer. Not because the AI is malicious — but because partial updates on complex objects are inherently dangerous when the tool doesn't distinguish between "replace this one thing" and "replace everything."
The find-then-set pattern isn't new. It's how most database transactions work. But in the MCP world, where tools are the API and the AI is the client, it's easy to forget that your tools need the same safety patterns your database does.
The formlm-cli MCP server includes field_set_property (accessible via formlm_exec) specifically for this use case. Open source at github.com/formlm/cli — try it at formlm.me.
Top comments (0)