Last time, we got Claude running inside Microsoft Foundry and said hello to it. Fun, but useless. A model that can describe Seattle is not going to help Harbor Supply Co. ship a single pallet.
So today Claude gets its first real job. We're going to take one of Harbor's support tickets, the kind that arrives as a half-typed mess at 4:58 on a Friday, and turn it into clean, structured data your systems can actually act on. No chat window. No human reading each one. Just messy text in, tidy fields out.
This is the moment the whole thing stops being a clever toy and starts being plumbing. And plumbing, if you've built Power Platform flows, is your home turf.
Meet a real Harbor ticket
Here's an actual message from Harbor's support inbox. Well, actual for a fictional company. You've seen a hundred like it:
hi so we ordered the heavy duty pallet racking (order HS-44821 i think?) and 2 of the uprights came bent, like properly bent not just scuffed. warehouse cant use them and were meant to be doing a big stock move THIS WEEKEND. need these replaced asap or at least know whats happening. called twice already. - Dawn, Westlake Logistics
A human reads that in three seconds and knows exactly what to do. The trouble is, a human has to read every single one.
Your flow can't. Power Automate has no idea whether this is urgent, which order it's about, or who should pick it up. A dashboard can't count it. A routing rule can't route it. As far as any of your systems are concerned, this is just a blob of text. Useful to a person, invisible to a process.
The job is to turn that blob into something a machine can read.
Decide what "clean" looks like
Before we ask Claude for anything, we decide the shape we want back. This is the most Power-Platform part of the whole exercise: you're designing the columns of a table, same as you always have.
For Harbor's tickets, I want six things out of every message:
- category: what kind of issue it is (damaged goods, delivery, billing, and so on)
- urgency: low, medium, or high
- customer: who sent it
- order_ref: the order number, if there is one
- summary: one plain line a human can scan
- needs_human: true when this is too thorny for an automated reply
That's it. Give me those six fields, filled in correctly, for any ticket Dawn or anyone else throws at us, and the rest of Harbor's automation suddenly has something to work with.
The obvious way, and why it bites
The first thing everyone tries is to just ask. You paste the ticket, add "reply with JSON containing category, urgency, customer, order_ref, summary, needs_human," and hit send. And honestly? Most of the time it works. Claude is good at this, and you'll get back something that looks perfect.
Most of the time is the problem.
When you're processing one ticket, "most of the time" is fine. When you're processing every ticket, automatically, with no human watching, the few that go sideways are exactly the ones that break your flow at 2am. The failures are small and annoying:
- A friendly sentence before the JSON: "Sure! Here's the structured data:" Your
json.loads()chokes on the first word. - The JSON wrapped in a markdown code fence, so now you're stripping backticks with string hacks.
- A seventh field Claude thought would be helpful, that your code wasn't expecting.
- "urgency": "pretty urgent" instead of one of your three allowed values.
None of these are hard to patch. But you end up writing a pile of defensive string-wrangling to clean up output that should have been clean in the first place. There's a better way, and it's built in.
The reliable way: give Claude a schema
Claude has a feature called structured outputs. Instead of asking nicely for JSON and hoping, you hand it a schema, and the model is constrained to produce valid JSON that matches it. Not "usually valid." Valid. No stray sentences, no code fences, no surprise fields.
The cleanest way to do this in Python is to describe the shape as a small class, then let the SDK handle the schema for you. Here it is, built straight on the AnthropicFoundry client from last time:
from anthropic import AnthropicFoundry
from azure.identity import DefaultAzureCredential, get_bearer_token_provider
from pydantic import BaseModel, Field
base_url = "https://<your-resource-name>.services.ai.azure.com/anthropic"
deployment_name = "claude-sonnet-4-6"
token_provider = get_bearer_token_provider(
DefaultAzureCredential(), "https://ai.azure.com/.default"
)
client = AnthropicFoundry(azure_ad_token_provider=token_provider, base_url=base_url)
# This is our table definition. Claude is forced to fill exactly these fields.
class Ticket(BaseModel):
category: str
urgency: str
customer: str
order_ref: str | None = Field(description="Order number if one is mentioned, else null")
summary: str = Field(description="One short line a human can scan")
needs_human: bool
ticket_text = """hi so we ordered the heavy duty pallet racking (order HS-44821 i think?)
and 2 of the uprights came bent ... need these replaced asap ... - Dawn, Westlake Logistics"""
result = client.messages.parse(
model=deployment_name,
max_tokens=512,
messages=[{"role": "user", "content": f"Extract the ticket fields:\n\n{ticket_text}"}],
output_format=Ticket,
)
print(result.parsed_output)
And what comes back is not a hopeful blob of text. It's a real Ticket object, already parsed and validated:
{
"category": "damaged goods",
"urgency": "high",
"customer": "Dawn, Westlake Logistics",
"order_ref": "HS-44821",
"summary": "Two pallet racking uprights arrived bent; replacement needed before a weekend stock move.",
"needs_human": true
}
That is a row. You could drop it straight into Dataverse, raise it in a queue, or hand it to a Power Automate flow without touching a single string function. The messy blob became clean data, and it will be clean every single time.
Tightening it up
The first version works, but a couple of fields are looser than I'd like. Building in the open means showing the two small changes that made it genuinely production-ready.
Pin down urgency. Right now urgency is a plain string, so one ticket says "high" and the next says "urgent" or "High-ish." I don't want to find out at 2am that my routing rule missed one. So I turn it into a fixed set of choices, and now Claude can only ever return one of three values:
from enum import Enum
class Urgency(str, Enum):
low = "low"
medium = "medium"
high = "high"
# in the model:
urgency: Urgency
Make the model admit when it's unsure. A ticket that just says "where is my stuff" with no order number shouldn't be guessed at. I add a short instruction in the prompt: set needs_human to true whenever a field is missing or the request is ambiguous. That one line turns needs_human from decoration into a real safety valve, so the awkward cases get a person instead of a confident wrong answer.
That's the whole loop. Decide the shape, run a few real tickets through it, and tighten the fields that wobble. You're not prompt-whispering; you're doing the same iterative tidying you'd do to any flow that has to survive real-world input.
A few things that will trip you
Short list, learned so you don't have to:
-
Check why it stopped. Look at
stop_reasonon the response. If it'smax_tokens, your output was cut off mid-JSON, so raise the limit. If it'srefusal, the model declined and the output may not match your schema, so don't blindly parse it. - Compare enums case-insensitively. A value can come back differing only in capitalisation ("High" vs "high"), so don't do a strict string match in your routing.
-
Objects need
additionalProperties: false. The Pydantic helper handles this for you, but if you hand-write a raw JSON schema, set it, or the request is rejected. - Keep real customer data out of the schema. Field names and descriptions are fine, but never bake an actual person's details into the schema definition itself. Harbor is fictional on purpose, and your real tickets should stay in the data, not the code.
Why this is the real unlock
Step back and look at what just happened. We took something only a human could use, a rambling message, and turned it into something a machine can route, count, and act on. That is the entire bridge from "chatbot" to "automation," and it runs right through the skills you already have. A Power Platform person who can design a clean table and reason about edge cases is exactly the person who's good at this.
Everything from here builds on this one move. An agent that answers tickets, escalates the hard ones, or looks up an order is really just this extraction with actions bolted on.
Next time we bolt on the first action. We'll take this tidy ticket data into Copilot Studio and stand up an actual agent that can answer a Harbor customer, grounded in Harbor's own world rather than whatever the model happens to remember. That's the post where the two halves of your brain, the business side and the AI side, finally meet.
If you run a few of your own messy tickets through this, I'd love to see what came back. Show me your worst one.
See you next time. 😊
Top comments (0)