DEV Community

Keo Fung | FormLM
Keo Fung | FormLM

Posted on

The find-then-set Pattern: How I Stopped My AI Agent From Breaking Fields

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 via formlm_exec — the direct command execution tool. The find-then-set pattern remains the recommended approach for fine-grained field updates. For bulk changes, formlm_modify wraps 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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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);
  // ...
});
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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.

  1. Call field_find to get the current field state
  2. Identify the exact property path that needs changing
  3. Call field_set_property with 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"
Enter fullscreen mode Exit fullscreen mode

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_update when you're replacing the entire structure of a field (new type, new options list, new title — a deliberate full replacement)
  • Use field_set_property when 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)