Skip to content
Article

How I Used AI and Python to Validate STORIS and Inntopia Data for Planning, Mapping, and Pattern Finding

I do not dump whole exports into a chatbot. I use an LLM to help write tight Python that answers one question at a time, prove it on a small sample, then run it locally larger data sets. That keeps work repeatable, cuts token cost, and keeps sensitive fields off hosted tools while I plan integrations and product mapping.
How I Used AI and Python to Validate STORIS and Inntopia Data for Planning, Mapping, and Pattern Finding

When people talk about using AI for data work, the conversation can get fuzzy fast. It starts to sound like you dump a giant export into a chatbot and somehow get clarity back out.

That is not what I have found most useful.

What has actually helped me is using AI to write small Python scripts that answer narrow questions well. Once the script works, I can run it locally against large datasets, inspect the output, refine the rules, and rerun it as many times as I need without spending more tokens each time.

That has been useful for me while working through data related to platforms like STORIS and Inntopia, and the same thinking applies to other systems with noisy exports or large API responses, including Klaviyo-shaped ecommerce data.

If you are trying to figure out whether someone really understands this kind of project work, this is the level where I have found the real value: not generic "AI for data" talk, but careful investigation, local scripting, and repeatable logic.

tl;dr

The basic idea is simple:

  • Define the problem or assumption you're trying to confirm

  • Use the LLM to help write the logic.

  • Test that logic on a small, known dataset. (Important, you're likely going to need to refine the logic)

  • Iterate until you're satisfied with the results

  • Run the script locally on the full payload. (Keep the giant raw data out of the chat once the script exists.)

Instead of asking an LLM to repeatedly read thousands of rows or huge nested JSON responses, you use it once to help build the tool. After that, your machine does the repetitive work.

Prerequisites

You do not need much to work this way:

  • A clear question you are trying to answer

  • A small sample of the data shape

  • Python installed locally

  • A way to run scripts from the terminal

  • A willingness to test and refine instead of expecting the first output to be perfect

That last one matters. In my experience, the first draft of the script is often close, but not quite right.

The Problem

Exports are messy. API payloads are often much larger than the specific question you are trying to answer. Planning questions also tend to be narrower than the available data.

You may not need to know everything about an order, booking, or product. You may only need to know:

  • whether an endpoint includes the cases you think it includes

  • whether a field maps consistently to another field

  • whether a product key can be decomposed into meaningful pieces

  • whether a subset of rows contains a pattern worth investigating

If you throw the full dataset at the model every time, you pay for that over and over. You also create more work for yourself if the raw data includes addresses, payment information, or other sensitive fields that you would rather not keep pasting into a hosted tool.

That is where the Python script becomes the real win.

The Process I Follow

Step One: Define the Problem

I start by naming the problem as plainly as I can.

i.e. Instead of asking "analyze this export," I define what it is I am trying to confirm or understand. Questions like:

  • Does this STORIS endpoint return only fully completed delivered orders, or can it include orders with incomplete fulfillments?

  • Can this Inntopia field be split into stable product identifiers I can use for mapping?

  • Are these rows noisy, or do they actually represent consistent business entities?

If the question is vague, the script will be vague too.

Step Two: Clarify What I Am Investigating

Once I have the problem, I turn it into a testable question... or conditions that may give me a better understanding of the picture. This part is tempting to skip, but it makes a huge difference.

For example, I am not just asking whether an order "looks delivered." I might actually be asking:

  • Does the STORIS api /Orders/DeliveredOn endpoint include orders with multiple fulfillments that have different status codes?

  • Can I find orders where one fulfillment has tracking and another does not?

  • Does the particular Inntopia ProductSourceSystemKey data I am investigating consistently split into a product family identifier and a more specific variant identifier?

That shift matters because now the logic can be expressed in conditions instead of vague intent.

Step Three: Give the LLM Pseudocode, Not Just Desired Output

Through trial and error I have learned I usually get better results when I give the model pseudocode upfront instead of only describing the output I want.

For some reason, AI can do a decent job inferring a lot of structure when asked to develop a feature, but it can still miss a very important concept in a small Python utility described in plain english. A short script does not leave much room for interpretation. If one branch is wrong, the whole result can be misleading.

So instead of saying:

give me a script that returns the right orders

I have better luck with something closer to:


Read JSON from a file.

Look at data.salesOrders.
For each order:
	ignore it if fulfillments is missing or has fewer than 2 entries
	check each fulfillment
	look at lineItems inside each fulfillment
	count whether any line item has a non-empty trackingNumber
	if the order matches the rules, keep orderId

Return only the matching orderIds.

That gives the model something much closer to actual control flow. It also forces me to think through the edge cases before I ever run the script. Yay! More token savings!

Step Four: Run a Control Experiment

Before I run anything against a giant file, I test on a small slice of data that I can inspect manually. That might be a handful of orders I already know something about, or a few rows from a CSV that I can reason through myself.

This is where I iterate over the script. I may expect my control to return 0 results and I get back 10, or I expect 10 and I get 0. Heck, maybe I even get an exception!

Maybe I forgot that a field can be blank. Maybe a list is sometimes missing. Maybe I assumed tracking exists at the fulfillment level when it actually lives on the line item. Maybe I need to preserve order or handle a special case. Maybe the generated logic is just plain wrong!

The point is to use the model to get to a good first draft faster, then use a control experiment to make sure the logic matches reality. i.e. "Trust, but verify."

Step Five: Scale Locally

Once the script behaves the way I expect, that is when the process becomes really useful.

Now I can run large payloads or very large CSV exports through the local script as many times as I want. No extra tokens for each rerun. No repeated copy-paste. No asking the model to reread the same giant payload because I changed one condition.

There is another practical benefit here too. If the local file contains sensitive fields like addresses, phone numbers, payment-related data, or other customer details, I am not using the model as the thing doing the actual processing. The heavy lifting stays in the local Python script.

That means I do not have to repeatedly clean and redact the full dataset just to ask the same analytical question over and over. (If you need to pass in the full dataset to your LLM, you can even build a custom script to redact the fields you need redacted!)

To be clear, I still prefer to use redacted snippets in anything public, in git, or in example prompts. But once the script exists, the runtime analysis can stay local.

Example One: STORIS Order Validation

One recent example was working with STORIS order data.

I had a lightweight response shape that returned order objects with enough information to get the orderId, including a type field:


"orders": [
	{ "orderId": "01234567", "orderType": 5, "type": 1 },
	{ "orderId": "0987654", "orderType": 0, "type": 1 },
	...
]

From there a script let me collapse that larger response into a simple comma-separated list of order IDs like '01234567,0987654,...'. That was useful because I could pass those IDs into another API call to fetch order details in bulk. If you only have 2 or 3 order ids to look at, this would be a waste of time....but if you're trying to find needles in a haystack and grab as many order IDs as you want? The trade-off quickly becomes apparent.

Next was the actual details of these orders I was attempting to fetch. For my needs, I wanted to understand:

  • Was api/Orders/DeliveredOn really returning only the kind of completed orders I expected?

  • Could it include orders with multiple fulfillments in different states?

  • Could I find orders where one fulfillment had tracking information and another did not?

These were the relevant fields to me.

{
	"data": {
		"salesOrders": [
			{
			"orderId": "01234567",
			"fulfillments": [
				{
					"status": 4,
						"lineItems": [
							{
							"trackingNumber": "T123456",
							"type": 1
							}
						]
					}f
				]
			}
		]
	}
}

vs the payload in its full glory (You’re gonna have a scroll a bit to get past this one!):

{
  "warnings": [
    {
      "warningMessage": "string"
    }
  ],
  "success": true,
  "transactionId": "75906707-8c31-479c-b354-aa805c4cefbc",
  "message": "string",
  "data": {
    "salesOrders": [
      {
        "orderTotals": {
          "balance": 211.23,
          "subTotal": 198.76,
          "discount": 10,
          "fees": 1,
          "install": 1,
          "tax": 2.99,
          "delivery": 14.99,
          "total": 332.49,
          "payments": 121.26,
          "protectionPlan": 50.26,
          "deposits": [
            {
              "depositDate": "2023-10-31T04:00:00Z",
              "depositAmount": 2.99,
              "depositType": "CASH",
              "depositTypeDesc": "CASH",
              "depositClass": 1,
              "depositNumber": "string"
            }
          ]
        },
        "fulfillments": [
          {
            "schedule": [
              "2022-04-08T04:00:00Z",
              "2022-04-09T04:00:00Z"
            ],
            "reasonNoScheduleDates": "Invalid - Order is on CREDIT HOLD.",
            "custom": {
              "deliveryStartTime": "string",
              "deliveryEndTime": "string"
            },
            "originalFulfillmentId": "59DF3FA0-6223-BF12-073B-9EF4B5EBB9C7",
            "fulfillmentId": "59DF3FA0-6223-BF12-073B-9EF4B5EBB9C7",
            "routingNumber": "7",
            "address": {
              "description": "5th Ave residence",
              "latitude": 40.774,
              "longitude": -73.966,
              "name": "John Edward Doe",
              "address1": "555 Fifth Ave.",
              "address2": "Suite #5",
              "city": "New York",
              "state": "NY",
              "zipCode": "10001",
              "country": "USA",
              "cellPhone": "2122134122",
              "workPhone": "2122135705",
              "workPhoneExtension": "501",
              "homePhone": "2121232111",
              "emailAddress": "[email protected]"
            },
            "status": 4,
            "date": "2021-05-18T04:00:00Z",
            "dateWithStopTime": "2021-05-18T04:00:00Z",
            "method": 2,
            "shippingInstructions": "Example shipping instructions.",
            "locationId": "88",
            "deliveryTicketPrinted": true,
            "pickTicketPrinted": true,
            "onManifest": true,
            "totals": {
              "total": 442.31,
              "fees": 1,
              "install": 50,
              "subTotal": 373.98,
              "tax": 12.34,
              "delivery": 14.99,
              "discount": 10
            },
            "lineItems": [
              {
                "brandId": "BRAND1",
                "description": "Example product description.",
                "id": "CHAIR1",
                "packageParentId": "KIT1",
                "loadedOnTruck": true,
                "poLineId": "2",
                "regularSellingPrice": 190,
                "normalPrice": 180,
                "price": 150,
                "quantity": 2,
                "quantityCommitted": 1,
                "quantityToDeliver": 1,
                "trackingNumber": "T123456",
                "vendorModelNumber": "D10000112326",
                "directShipTrackingNumber": "D101A598Z987898-0",
                "imageURL": [
                  "https://www.examplesite.com/images/CHAIR1-sm",
                  "https://www.examplesite.com/images/CHAIR1-med",
                  "https://www.examplesite.com/images/CHAIR1-lg"
                ],
                "specialOrderOptions": [
                  {}
                ],
                "comments": "Example line item comments.",
                "group": "CHAIRS",
                "lineNumber": 2,
                "linkedToService": [
                  {}
                ],
                "reasonCodes": [
                  "RC1",
                  "RC8"
                ],
                "serialNumbers": [
                  "121659889-5656",
                  "46555226-556"
                ],
                "serviceLineNumber": 0,
                "stockLocationId": "66",
                "type": 1,
                "vendorId": "ABC1",
                "unloadTime": 20,
                "volume": 10,
                "weight": 65.6,
                "autoTransferId": "T01234567*1",
                "autoTransferQuantity": 10,
                "protectionPlan": {
                  "code": "abc123",
                  "description": "extended warranty",
                  "price": 180,
                  "uniqueIdentifier": "D8E229E2-0238-3FBE-4DB1-022304B4E1BA"
                },
                "transferInformation": [
                  {}
                ],
                "discounts": [
                  {}
                ]
              }
            ],
            "description": "Example description.",
            "handlingMethod": {
              "id": "HND1",
              "description": "Example handling method description."
            },
            "requestedDate": "2021-05-18T04:00:00Z",
            "routeCode": "RC1",
            "truckNumber": "T511",
            "contact": {
              "status": "ABC",
              "date": "2021-05-18T04:00:00Z",
              "name": "Mrs. Smith"
            }
          }
        ],
        "fraudSessionId": "string",
        "customerPONumber": "PO12345",
        "discounts": [
          {
            "code": "DISC2",
            "amount": 15.99,
            "percent": 25
          }
        ],
        "orderId": "8898765",
        "customerId": "8812345",
        "billingAddress": {
          "firstName": "John",
          "middleName": "Edward",
          "lastName": "Doe",
          "prefix": "Mr.",
          "suffix": "Jr.",
          "description": "example description",
          "address1": "123 First St.",
          "address2": "Unit #9",
          "city": "New York",
          "state": "NY",
          "zipCode": "10001",
          "cellPhone": "2123124122",
          "homePhone": "2121232111",
          "workPhone": "2122135705",
          "workPhoneExtension": "501",
          "emailAddress": "[email protected]"
        },
        "locationId": "88",
        "orderType": 1,
        "orderDate": "2021-05-18T04:00:00Z",
        "quoteExpirationDate": "2021-05-18T04:00:00Z",
        "creditHoldCodes": [
          "R1",
          "S1"
        ],
        "salespeople": [
          {
            "id": "JAD",
            "name": "Jane Doe"
          }
        ],
        "companyId": "123",
        "originalOrderId": "8844987",
        "protectionPlans": [
          {
            "code": "abc123",
            "description": "extended warranty",
            "price": 180,
            "uniqueIdentifier": "D8E229E2-0238-3FBE-4DB1-022304B4E1BA"
          }
        ]
      }
    ],
    "orderErrors": [
      {
        "orderId": "8898765",
        "errorMessage": "Example error message."
      }
    ]
  },
  "serverTime": "string"
}

That detail payload is HUGE! Each order can include totals, addresses, multiple fulfillments, line items, tracking numbers, protection plans, discounts, and so much more... and that is before you multiply it by hundreds or thousands of orders!

That is where one of my scripts became useful. Instead of rereading huge payloads in a chat, I could encode the rules once and return only the order IDs that matched the cases I cared about.

So the workflow became:

  • Pull the lighter order list.

  • Reduce it to a usable string of IDs.

  • Request the detailed payload for those IDs.

  • Run the detailed payload through the local filter script.

  • Review only the IDs that matched the conditions I was investigating.

That is a much better use of AI than pasting hundreds of large order objects into a conversation and asking the model to keep track of them. Not to mention the topic of AI temperature. We may ask it multiple times and get different answers or even hallucinations.

When I'm planning a project or task, I want to make sure I can get consistent, repeatable results so I can define what it is I'm trying to build accurately.

Example Two: Inntopia Mapping and Product Keys

Another good example came from Inntopia-related activity and reservation exports.

This was less about understanding an endpoint and more about integration planning.

I was trying to understand how fields could map across systems, whether those mappings were consistent, and how I could create a more unified mirror of products for downstream use.

The export was huge. After a cursory pass, I had a rough idea of which sources and row types actually mattered for what I wanted to learn. I used an AI-generated script to apply that logic and strip out thousands and thousands of rows I did not need. That turned an unwieldy file into something I could actually look at without drowning.

Trying to do the same thing in Google Sheets would have been painful. Sorting and filtering that many heavy rows in the browser is slow. The spreadsheet ends up eating system memory until everything feels like molasses. Running the filter locally in Python was faster, repeatable, and easier to tweak when I refined the rules.

After that, I could focus my energy into manually reviewing relevant data columns and looking for consistency patterns.

One field that mattered a lot was ProductSourceSystemKey.

Through that review, I found that for our most important data source with thousands of product offerings, a value like 123456.987654 was not just an opaque string. It could be interpreted as two meaningful pieces:

  • 123456 identified the "Product Line of Business" (Think a type of grouping or category), such as a one-day bike rental

  • 987654 identified a more specific subtype, such as the exact bike variant for that line item (i.e. kids bike, 24", 26", stroller, helmet, etc.)

That matters because now the string is not just an arbitrary key from an export but rather something you can reason about.

Now I can use it to:

  • confirm consistent mappings

  • group multiple variants under one logical product

  • keep frontend messaging tied to a stable product entry

  • support custom title overrides or description overrides

  • carry location context more cleanly through an API layer

Another script then helped me read the data exports, split the key, and create a deduplicated identifier table that is easier to reason about than a giant raw dataset.

The surrounding scripts helped with the other parts of the job too:

  • One to act as a quality gate on raw exports

  • One to group and sort related activity records

  • Another to a complete a sanity check on daily event counts so I am not building assumptions around a weird outlier day

What I like about that set of scripts is that they helped me to answering real planning questions without needing to redact personal data and use up precious data limits.

Why This Saved Me Time

The obvious answer is that it saved manual work.

There was a learning curve and some time spent on the technique of building scripts, but the payoff was I did not have to keep scanning giant payloads by eye, copying values into temporary spreadsheets, or redoing the same filtering logic every time a new export came in.

The more interesting answer is that it changed where the effort went. Instead of spending energy on repetitive inspection, I spent it on:

  • defining the problem well

  • writing better pseudocode

  • testing assumptions on a small control sample

  • refining the logic until it behaved correctly

Once that was done, reruns were cheap.

Why This Saved Tokens

The LLM was most useful when I used it to help me think through structure, conditions, and edge cases. It was much less useful as a bucket to pour full datasets into over and over.

Once the script exists, I do not need to spend tokens every time I want to:

  • change one condition

  • rerun the logic against fresh data

  • test a slightly different subset

  • validate a new export from the same source

The cost gets front-loaded into the script design and the early refinement. After that, the local Python script does the repetitive work.

Why This Helped With Data Handling

There is also a very practical security and privacy benefit.

If I am processing the full dataset locally, I do not need to keep preparing fully sanitized versions of it for every analysis pass. That matters when a payload contains addresses, phone numbers, payment-related fields, or other data I would rather keep out of a hosted tool unless there is a very specific reason to share a tiny redacted sample.

That does not remove the need for good judgment. Public examples should still be redacted. Anything committed to git should still be safe. But local execution lets me separate script generation from full-data processing.

What Someone Else Could Take From This

If you want to use this same process, I would suggest starting smaller than you think.

Do not begin with the whole export, go back to programming 101 and start with:

  • one sharp question

  • one small sample

  • one pseudocode description of the rules

  • one control test

  • one local script that can scale once it is proven

That approach has been useful for me in STORIS and Inntopia work, and it is the same kind of thinking I would bring to other integration-heavy platforms as well, including Klaviyo-related data flows where the challenge is less about one flashy feature and more about getting the data into a shape you can trust.

The point is that that AI does not magically understand your business logic and needs. if you give it a clear problem and clear control flow, it can help you build the small tools that make large datasets much easier to process with your own organic brain.