Skip to content
Keboola Docs

Transform data

Create an SQL transformation that joins a summer of sales, rosters and weather with a live forecast into one table per café and day, and learn how mappings keep Storage safe.

Tutorial Getting Started about 15 minutes

The sales table knows how many cups each café sold on each day of the summer. The weather table knows what kind of day it was. The roster knows how many people were on shift, and the forecast knows what next week looks like. None of them knows what the others know. This step joins all five with SQL into one table, staffing_outlook: 42 rows, one per café per forecast day, each saying how busy that day should be, who is rostered, and whether that is enough. It also introduces the mapping model that keeps your source data safe while you do it.

Where you are: six raw tables are in Storage and nothing has been joined yet. When you finish: one table that answers the question, and a transformation you can re-run.

Before you start

You need

  • A Keboola project. No project yet? Get a project.
  • The five tables it reads, in Storage from Get your data in: stores, sales, staffing and weather_daily in one bucket, forecast in another.

Depending on the tab you use

  • Prompt tab: Kai, the Kai Agent button in the project's top bar, on supported stacks. The first time, an organization admin switches it on; see Get started with Kai.
  • UI tab: only a browser.
  • CLI / API tab: kbagent, connected to your project with write access.

Three ways to do this, one page. They are alternatives, not steps. Prompt is what you paste into Kai; UI is the same task click by click, for when you want to see where each setting lives; CLI / API is the same task from a terminal, with the raw request beside it. Pick one tab and carry on; your choice follows you to the next page.

Kai asks before it changes anything: expect one approval dialog per object it creates, showing the exact configuration it is about to write. Questions that only read do not ask. If confirming each one gets tiring, pre-approve tools in tool permissions. The chat's plan mode button is a different thing: Kai writes the steps down and asks you to approve the plan before it starts, and then still asks for each change. Use it when you want to read the plan first, not to save clicks.

If a step builds something other than what this page describes, say so in the same chat; it edits what it made. Nothing here is one-way.

The bucket names do not matter, and they contain configuration IDs, so yours will not match the screenshots. What the SQL depends on is the Table name you give each table in the input mapping below: those must be exactly sales, weather_daily, staffing, stores and forecast, or you have to edit the queries to match. (products is not needed for this table.)

A transformation never runs against your Storage tables directly. Keboola copies the tables you ask for into a temporary database schema, runs your queries there, and copies back only the results you ask for. Three settings control that:

  1. Input mapping decides which Storage tables get copied in, and what they are called inside the transformation. Anything you do not list is not visible to your code.
  2. Output mapping decides which tables your code produces get written back to Storage, and where. Anything you do not list is thrown away when the job ends.
  3. Queries are the SQL itself, organized into named code blocks.

That is the safeguard: the only tables your transformation can change are the ones named in the output mapping. It is also what lets Keboola track data lineage across the project.

Screenshot - How mapping works

Four ideas, and the queries below are those four in order:

  1. Daily demand. Sum units per café per day, and put that day’s temperature and rain next to it.
  2. Weather bands. Sort every summer day into a temperature band (under 18 °C, 18 to under 23, 23 to under 28, 28 and over) and a rain band (dry under 0.5 mm, wet otherwise), then average the daily demand per café per band. That average is what a day like that has produced before.
  3. Roster. From staffing, one headcount per café per weekday. The chain rosters by weekday, so this is a single value for every combination.
  4. The outlook. Put each forecast day into its bands, look up the café’s average for that band and the roster for that weekday, divide one by the other, and flag anything over 67 cups per person.

The bands are half-open: < 18, then < 23, then < 28, then everything else, and a day at exactly 23.0 °C belongs to exactly one of them. The same CASE appears twice in the query, once for the summer and once for the forecast, and the two have to agree to the character: a forecast day sorted into a band the history side never produced finds no average and comes out empty. Nine summer days sit exactly on 18, 23 or 28 °C and twelve exactly on 0.5 mm, so where the boundary falls is not academic.

Before either tab: find out which SQL your project speaks. Open Transformations and click Create Transformation; the New Transformation dialog lists what this project can run, and the SQL entry is either Snowflake SQL Transformation or Google BigQuery Transformation. New Free Plan projects default to the BigQuery backend; contract customers choose theirs. Close the dialog and carry that word into whichever tab you use.

Writing SQL transformations is Kai’s home ground (SQL Transformations), and it already has your table schemas in front of it. Open Kai Agent in the top bar and ask:

Prompt
Build a Snowflake transformation called "Boolabean staffing outlook" with one output table,
staffing_outlook. Storage columns arrive as text, so cast before you compare or average.
1. Daily demand: from sales, sum units per store per date, and join weather_daily on store_id
and date.
2. Weather bands: temperature under 18, 18 to under 23, 23 to under 28, and 28 and over, each
split into dry (rain under 0.5 mm) and wet (0.5 mm or more). Use half-open ranges so no day
lands in two bands. Average the daily units per store per band.
3. Roster: from staffing, the rostered headcount per store per weekday. It is the same every
week, so this is one value per store and weekday.
4. Forecast: the forecast table's location_id 0 to 5 maps to store_id S01 to S06 in that order,
the order the coordinates appear in the request URL.
5. Output one row per store per forecast day with exactly these columns: store_name, city,
date, weekday, forecast_temp_c, forecast_rain_mm, expected_units (the store's historical
average in the matching band), rostered_staff, expected_units_per_person, and short_handed,
true when that figure exceeds 67.
Run it, then tell me how many rows staffing_outlook has and which store and day come out most
stretched.

On a BigQuery project, say “BigQuery transformation” in the first line; everything else stands. The column list in point 5 is what the later pages type into their prompts and checks, so keep it as written.

Spelling out the five parts is the point. Ask only for “a staffing table” and you get a different set of columns, and the check below will not match. The half-open sentence matters most, and the cast sentence second: Kai samples the tables first to see the date format and the text values, and then casts everything it compares.

Kai builds the transformation on read-only input: it reads the source tables by their full Storage names and leaves the input mapping empty, so you will not see one in its configuration. The mapping sections of the UI tab describe the hand-built path; the result is the same table. Expect two approvals, one for the transformation and one for the run.

Check: Kai reports staffing_outlook with 42 rows and names the most stretched café and day. If the count differs, the joins differ: re-prompt, or fix the code in place. Later steps depend only on the table name, so a close-enough table is fine.

Coming from the Prompt or CLI / API tab? The run already happened: the prompt asked for it, and job run --wait did it. Skip to the row-count check below.

Click Run Transformation and confirm with Run. That creates a background job which copies the input tables in, runs your SQL, and writes staffing_outlook back to Storage: the mapping model from the top of this page, in action.

Screenshot - Running the transformation

A notification appears with a Show job link (Snowflake SQL job has been scheduled, or your backend’s equivalent) and you can also find it under Jobs. It takes about a minute, and a green Success means it worked.

Screenshot - Successful job

Then open Storage: there is a new bucket out.c-Boolabean-staffing-outlook, listed as Boolabean-staffing-outlook with an OUT badge the same way the in.c- prefix was hidden before, holding staffing_outlook with 42 rows and 10 columns.

Three checks, in order of how much they tell you:

  • 42 rows, which is six cafés times seven forecast days. More than 42 means an input table has duplicate rows (a forecast loaded twice with incremental load on, a café listed twice); fewer means a join dropped a café or a day, most often because location_id did not map onto a store_id.
  • No two rows share a café and a date. Open the Data Sample tab and sort by store_name; each café should show seven different dates.
  • short_handed is true on a few rows, not on all and not on none. Sort by expected_units_per_person descending: the top row is the café and day the whole guide is about, and the next page asks Kai to explain it. How many rows are flagged depends on the forecast; our week had four, and a cool, wet week can flag nothing, which is the table being right.

The 10 columns are store_name, city, date, weekday, forecast_temp_c, forecast_rain_mm, expected_units, rostered_staff, expected_units_per_person and short_handed. If Kai built the table, Storage may show them in upper case: Snowflake upper-cases an alias that is not quoted, and the names are the same names. An empty expected_units on a row is not an error: it means the summer never had a day in that café’s band, so there is no history to average. In this sample that band is cold and dry: five of the six cafés had no dry day under 18 °C all summer.

The table list also has a Recently Updated By column, naming the transformation and its backend (Boolabean staffing outlook / Snowflake SQL here), the fastest way to answer “where did this table come from?” months later.

In Storage, the table list shows it:

Screenshot of the Storage table list with staffing_outlook and its Recently Updated By column naming the transformation

Running the transformation again rebuilds the table; it is safe to re-run while you are experimenting. Tomorrow’s forecast gives you tomorrow’s table, which is what the scheduling page is for.

  • Object 'SALES' does not exist (Snowflake). A table is missing from the input mapping, or the Table name inside the transformation differs from what the SQL uses. Snowflake upper-cases unquoted identifiers, which is why every identifier is double-quoted. On BigQuery the equivalent error is Table ... was not found.
  • The job succeeds but Storage has no new table. The output mapping is empty or names a table your SQL never creates. The names must match exactly: staffing_outlook.
  • Failed to process output mapping … Some columns are missing in the csv file. Missing columns: STORE_NAME, CITY, … The table already exists in Storage, with column names in a different case from the ones your query produced this time. An existing table remembers the exact names of its columns, and Snowflake upper-cases an alias you did not quote, so switching between quoted and unquoted aliases between runs is enough to trigger it. Either keep the alias style that created the table, or delete the table in Storage and run again. It happens when the Prompt tab created the table first and the hand-written query writes into it, and the other way round.
  • Numeric value '' is not recognized. Tables loaded from CSV arrive as text columns unless you give them types, so an empty cell is '' rather than NULL. The queries cast with TRY_TO_ (Snowflake) or SAFE_CAST (BigQuery) before comparing; if you see this error, a cast got dropped somewhere.
  • The table has more than 42 rows. An input has duplicates. The forecast configuration ran with Incremental load on and appended a second copy, or stores lists a café twice. The joins cannot do this on their own; look at the inputs, and switch incremental load off on the forecast row.
  • Every row has an empty expected_units. The band CASE on the forecast side does not produce the same labels as the one on the history side, so nothing joins. Compare the two expressions character by character.
  • Invalid columns: _timestamp when the output is written. Your input tables were staged by cloning, which copies Keboola’s internal _timestamp column along with the data, and that column cannot be written back to Storage. It is not backend-specific and it is not a problem with the query. The clean fix is the dropTimestampColumn option on the input mapping; see the _timestamp system column.
  • You want to see what the query actually returns before saving. That is what a workspace is for.
  • You would rather not read the log yourself. Kai has it open already (Troubleshooting): My transformation "Boolabean staffing outlook" failed. Read the last job's log and tell me what to fix.
  • Use a workspace to develop and test queries against a copy of the data before committing them to a transformation. This is how the work is really done.
Ask Kai

Hi, I'm Kai — Keboola's AI assistant for the docs. Ask me anything and I'll answer from the documentation and cite the pages I use.

Kai is an AI and can make mistakes. Check the sources it links.