Skip to content

Training Build a Weather Lookup Screen V2 Store Readings

Craig Scott edited this page Jun 12, 2026 · 2 revisions

Training: Weather Lookup V2 — Store the Readings

Weather Lookup series · Part 2 of 3 · Level: Intermediate.Part 1 — Build the Screen (beginner) · Part 3 — Watched Locations & Scheduled Capture → (advanced).

Add-on to Training: Build a Weather Lookup Screen. Build V1 first — this picks up exactly where it left off and assumes you have the working Weather API connection, the deployed Weather Lookup Flow, and the Weather Lookup screen.

What you'll learn: how to turn a one-shot lookup into something that remembers — add a data model, write each result into it from the flow, dedupe by ZIP + date so the same lookup updates instead of piling up duplicates, and show a running history on the screen. You need: Administrator or Developer access type. V1 completed. Time: 30–40 minutes.

Where V1 left off (the recap)

In V1 the user types a ZIP, clicks Get Weather, the form's data transform runs $executeFlow('weatherLookupFlow', { zip }), and the response is rendered as markdown. Nothing is saved — every lookup is throwaway.

ZIP ─▶ Get Weather ─▶ Form.loadData ─▶ $executeFlow(flow) ─▶ wttr.in ─▶ markdown on screen

V2 adds persistence: the flow also writes the reading into a data model, keyed so the same ZIP on the same day updates one row instead of inserting many, and the screen shows the saved history.

ZIP ─▶ Source ─▶ Set Context (stash zip) ─▶ HTTP ─▶ JSONata reshape ─┬─▶ Response (weather → screen)
                                                                     └─▶ Mutate (UPSERT WeatherReading, key = zip|date)

                              Screen history table (WeatherReading)

Level 1 — Create the WeatherReading data model

Use the Application Designer for models, not the old Schema Designer. In Fuuz, build data models from App Designer → Data Model (or open a saved one from the Workspace tree). The standalone "Schema Designer" canvas is the legacy tool.

  1. Create the model. App Designer → Data Model → New Model: Name WeatherReading, Module Training Module (a module is required — it's also what makes the model appear in the App Designer Workspace tree), Type Transactional.

  2. Add the fields (App Designer → the model → Field):

    Field Type Notes
    key String! the dedup key, zip|datemark it unique (below)
    zip String! the looked-up ZIP
    date Date! observation date
    location String city
    region String state/region
    conditions String current conditions
    windDir String wind direction
    tempF Int current temp °F
    feelsLikeF Int feels-like °F
    humidity Int %
    windMph Int mph
    forecast JSON the 3-day forecast array, stored whole
  3. Add a unique index on key. In the model's indices, add { name: "unique_key", unique: true, fields: ["key"] }. This is the crucial bit: a unique field generates a WeatherReadingWhereUniqueInput { key }, which the upsert below matches on. Same key → update; new key → insert.

  4. Deploy the model. (V1's lesson holds: nothing is callable until deployed.)

Why key = zip\|date and not a compound index? Upsert matches on a WhereUniqueInput, which is built from single unique fields. Packing ZIP and date into one unique key string gives the upsert a single field to match — the cleanest dedup.

Level 2 — Write to the model from the flow (upsert)

Open Weather Lookup Flow in the flow designer.

The ZIP problem — and why we use flow context. You'd think you could just read the ZIP back out of the weather response, but wttr.in doesn't echo the US ZIP you sent — its request[0].query comes back as a resolved "Lat 34.08 and Lon -118.35". The clean ZIP only exists on the flow's input, and by the time the reshape runs, the HTTP response has replaced the payload. The fix is to stash the ZIP in the flow context before the HTTP call, then read it back out in the Mutate. Flow context persists across nodes, including across the HTTP request.

  1. Stash the ZIP in context. Add a Set Context node (Toolbox → Context → Set Context) right after the Source, wired Source → Set Context → HTTP (remove the old Source → HTTP link so the chain runs through Set Context). Set its Transform to:

    { "zip": zip }
    

    This writes { zip: "90210" } into the flow context. Use zip (the field), not $$ would store the whole payload object under zip, and concatenating an object into the key later throws an error (which surfaces, confusingly, as a Mutate error: Variable "$payload" … was not provided).

  2. Add a Mutate node after the reshape (Toolbox → Fuuz → Mutate). Configure:

    • API: Application
    • Mutation:
      mutation ($payload:[WeatherReadingUpsertPayloadInput!]!){
        upsertWeatherReading(payload:$payload){ id key }
      }
    • Variables Transform (builds the upsert payload — pulls the ZIP from context, everything else from the reshaped weather):
      {
        "payload": [{
          "where":  { "key": $state.context.zip & "|" & date },
          "create": {
            "key": $state.context.zip & "|" & date,
            "zip": $state.context.zip, "date": date,
            "location": location, "region": region, "conditions": conditions,
            "windDir": windDir, "tempF": $number(tempF), "feelsLikeF": $number(feelsLikeF),
            "humidity": $number(humidity), "windMph": $number(windMph), "forecast": forecast
          },
          "update": {
            "location": location, "region": region, "conditions": conditions,
            "windDir": windDir, "tempF": $number(tempF), "feelsLikeF": $number(feelsLikeF),
            "humidity": $number(humidity), "windMph": $number(windMph), "forecast": forecast
          }
        }]
      }
      
      $state.context.zip reads the value the Set Context node stashed; $number(...) casts the API's string values to the model's Int fields; date comes from the reshaped output.
  3. Wire reshape → Mutate. The flow still returns the reshaped object to the screen (the markdown still works); the Mutate runs the upsert as a side effect.

  4. Re-deploy the flow.

Upsert = dedup. Because the upsert matches on the unique key (zip|date), running the same ZIP twice on the same day updates the one row; a different ZIP or a new day inserts a new row. No duplicate combinations.

Editor tip. Typing $state.context.zip into a Fuuz code editor pops a member-completion list that can swallow the rest of your line. If that bites you, write it as $lookup($lookup($state,"context"),"zip") — same result, no dots to trigger the popup.

Debugging tip. The flow designer's Console prints a per-node trace (each node's input, output, and context) when you run the flow. It's the fastest way to see whether $state.context.zip is set where you expect.

Level 3 — Show the history on the screen

Open the Weather Lookup screen.

  1. Drag a Table (Data palette) onto the screen and set its Data Model to WeatherReading, API Application, Auto Load on. (A model-bound table works fine — only a model-less table errors.)
  2. Tell the query which fields to fetch. In the table's Data → Additional Query Fields, list every field your columns need:
    ["id","key","zip","date","location","region","conditions","windDir","tempF","feelsLikeF","humidity","windMph"]
    This is the step that's easy to miss: a column's Data Path sets what to display, but the table only queries the fields in this list (it defaults to just ["id"]). Leave a field out and that column comes back blank.
  3. Add columns by dragging a Table Column into the table for each field — date, zip, location, region, conditions, tempF, feelsLikeF, humidity, windMph, windDir. The column's field picker is single-select, so set each column's Data Path and Column Label directly (one column per field). String and Int fields display fine with the default Text format.
  4. Fix the Date column. A Date field shows blank under the default Text format. Select the Date column → Behavior → Format = Date, and set a Time Zone (e.g. Fuuz Setting) — a Date won't render without one.
  5. Give the table a fixed Height (e.g. 400px) so it doesn't collapse, and optionally, after the Get Weather button's loadData step, call $components.Table2.fn.search() so the history refreshes immediately after a lookup (otherwise it refreshes on the next screen load / Auto Load).
  6. Save + deploy the screen.

Run it

Look up 90210. Look it up again — the history table still shows one row (the upsert updated it). Look up a different ZIP like 33139 — a second row appears, storing the ZIP you typed. The table is your audit trail, deduped by ZIP + day.

Nuances at a glance

Nuance What to do
Where to build models Application Designer → Data Model (not the legacy Schema Designer).
Model not in the Workspace tree Assign it a Module — the App Designer Workspace groups by module.
Dedup Unique key field = zip|date + upsert{Model} matching on key.
API returns strings, model wants Int Cast with $number(...) in the Mutate variables transform.
Getting the entered ZIP into the row wttr.in doesn't echo the ZIP — stash it with a Set Context node ({ "zip": zip }) before HTTP, read it back in the Mutate via $state.context.zip.
Set Context stores an object, not the value Use { "zip": zip } (the field), not { "zip": $ } (whole payload) — the latter breaks the key concat and shows up as a Mutate "$payload not provided" error.
$state.context.zip autocomplete eats your typing Write it as $lookup($lookup($state,"context"),"zip").
A column is blank even though its Data Path is set Add the field name to the table's Data → Additional Query Fields list. Data Path = what to display; that list = what the query actually fetches (defaults to just ["id"]).
Date column renders blank Set the column's Format = Date and a Time Zone (e.g. Fuuz Setting). The default Text format can't render a Date.
Table collapses to a thin bar Give the Table a fixed Height (e.g. 400px).
Nothing persists Re-deploy the model and the flow after the changes.
History won't refresh Call $components.Table2.fn.search() after the lookup (or rely on Auto Load to refresh on screen open).

Things you could add next

  • Trend view — chart tempF over date for a ZIP using the stored history.
  • Scheduled capture — a Flow Schedule that upserts readings for a set of ZIPs every morning, building history without anyone opening the screen.
  • One row per forecast day — promote forecast from JSON into a child ForecastDay model with a relation, for true per-day querying.

Next in the series

Part 3 — Watched Locations & Scheduled Capture You still have to open the screen and type a ZIP for anything to happen. Part 3 makes it hands-off: a WatchedLocation model related to your readings, a scheduled flow that loops every location and captures the weather on its own, a threshold alert, ZIP validation, and a trend chart.

Related training

Training: Build a Weather Lookup Screen (V1) · Training: Create a Data Model · Training: Create a Data Flow · Training: Explore the GraphQL API

🏠 Home

Getting Started (14)
Training Guides (52)

Applications

Access & Users

Data Models & Schema

Screens

Weather Lookup Series — guided 3-part build

Data Flows & Integrations

Data, Reporting & Monitoring

Enterprise & Organizations

Platform Concepts & Architecture (10)
Screens & Application Design (17)
Data Models & Schema (8)
Data Flows & Scripting (51)

Designing Flows

Data Flow Nodes

JSONata Reference

Scripting

Integrations & Connectors (30)

General & iPaaS

Plex

EDI

IIoT & Edge Gateway (18)

Physical Device Connectors

Edge Data Connectors

Reporting, Documents & Dashboards (8)
Administration & Access Control (27)
Data Management (8)
Accelerators, Templates & Packages (8)
Design Standards (1)
How-To Guides (8)
FAQ & Troubleshooting (1)
Release Notes (117)

2026

2025

2024

2023

2022

2021

2020

Policies & Company (6)

Clone this wiki locally