Help
Step-by-step guides for every AppFrunk app — search, or pick an app below.
🔎
Database & reports
Work with your own cloud SQL databases in a spreadsheet-style grid, build relational reports and dashboards, import files, check data quality, and ask an AI assistant grounded in your live data.
Last updated August 2, 2026 (EST)
Open Database & reports ↗- Shows live counts: Providers (distinct cloud DB providers connected), Databases (connections), and Tables (across all connections).
- Click a card to jump to your Databases or to Browse.
- Go to SQL Settings → Add a database. The wizard has 4 steps and remembers your progress.
- 1) Azure account — pick 'I have one' or follow the links to create a free Azure / Microsoft 365 account.
- 2) Database — use an existing Azure SQL database or create a new one; make sure the server's Networking allows Azure services.
- 3) Grant access — click 'Grant access with Microsoft' (a one-time admin-consent popup for the 'AppFrunk SQL Connector'), ensure a Microsoft Entra admin is set on the SQL server, then run the copy-paste grant script in the database's Query editor. Tick 'I've completed steps 1–3'.
- 4) Connect — enter your server host and database name (no password needed), choose read/write or read-only, and click 'Test & connect'.
- If your Azure admin is a different person: on the Grant-access step use 'Is your Azure admin someone else?' to copy an approval link to send them, then paste your Directory (tenant) ID (Azure Portal → Microsoft Entra ID → Overview) to confirm. If Connect says AppFrunk isn't approved yet even though access was granted, paste that same Directory ID on the Connect step and try again — it verifies the approval without redoing anything.
- SQL Settings lists each connected database.
- Validate now re-tests a connection and shows when it was last validated.
- Edit opens a popup to change the display name, server host, database name, permission scope, or default database. Changing the server or database re-checks the connection with AppFrunk's identity (make sure it has access there). 'Add a database' stays for brand-new entries.
- Toggle a connection between read/write and read-only.
- Remove deletes only AppFrunk's saved connection — your Azure database and the connector login are untouched.
- Optional, default off. Limit which IP addresses can use AppFrunk to reach a database: pick the database, turn on 'Allowed IPs for AppFrunk users', and add IPs or CIDR ranges ('Add my IP' fills in your current one). You can also restrict individual tables to specific IPs.
- Database firewall (Azure SQL): view and add/remove your database's own firewall rules from AppFrunk. To let AppFrunk change them, tick 'let AppFrunk manage this database's firewall' on the Grant-access step (it adds GRANT CONTROL ON DATABASE to the script).
- Database-level rules ADD allowed IPs and are checked before server rules — they cannot remove access already granted by broad server rules (like 'Allow Azure services'). To fully lock a database to only certain IPs, also narrow the server-level firewall.
- Server firewall (advanced): manage server-level rules from AppFrunk by entering your subscription ID + resource group and assigning the 'AppFrunk SQL Connector' the SQL Server Contributor role in Azure.
- Open Browse. Pick a database (searchable), then search tables & reports and filter by All / Tables / Reports.
- Organize with Workspaces (shareable table collections) and personal Favorites (folder tree).
- Open a table to edit data in a fast grid — sort, filter, search, edit cells, and save straight back to your database. You can also edit the table structure or delete the table.
- Row order: every table keeps its own saved row sequence. Drag a row by the handle on its left, right-click a cell and choose Insert row above or Insert row below to add a new row directly above or below it, or open a row and type a row number to move it any distance. Sorting by a column overrides the sequence while the sort is on; clear the sort to get back to it.
- System columns: a table can carry a read-only Created Date (when the row was created) and Created By (who created it: the person who added the row, submitted the form, or set up the import that loaded it). Add either from Table Settings, Column Settings, Type. Both can be hidden or renamed, never edited.
- Frozen rows: once a row's work is done (say, payroll processed), an admin of the table can FREEZE it — open the row and press Freeze row next to Delete row, or select rows and use Freeze in the Bulk Editor, or set up an automation whose action is Freeze the row. A frozen row shows a lock by its row number and in its edit panel title (the Freeze button stays highlighted while it is frozen) and keeps its values: cell edits, bulk edits, deletes, imports (an update doesn't overwrite it, a replace-all doesn't remove it), automations and recalculation don't change it, and its calculated and lookup columns keep the values they had. Changes an import or bulk edit had for frozen rows are noted in the table's Activity. A snapshot restore that would replace the table waits until the rows are unfrozen. Unfreeze the same ways (admins of the table only); the row's calculations are then brought up to date. Each freeze and unfreeze, with its reason, is in Activity.
- Cell history: right-click a cell and choose Cell history to see how that ONE cell got its value: the row's creation (who, when, and what the cell started with), then every change since. It shows value changes only; changing a computed column's formula is a change to the COLUMN, and appears in Table Settings, Activity instead. There a recalculation is ONE line; its Show N cells button lists each cell it changed (row, column, old → new), a page at a time.
- How quickly an import picks up a change: an import that is ACTIVE syncs within seconds of an edit at the source — the source notifies AppFrunk directly, and a background check every half hour makes sure every active import has that notification registered (re-registering it if the source ever drops it, including one per sheet when an import stacks several). A PAUSED import has none, by design. So there is nothing to switch on: if an import seems slow, check that it is enabled on the Imports page.
- Rebuilding source logic in Data (and retiring an import): a column filled by an import can be turned into a CALCULATION or a LOOKUP — open its type picker in Column Settings and choose Computed, or ask the assistant to. The column keeps its name; its stored values are replaced by the live value. From the next sync on, the import SKIPS every column that is computed or linked in Data (the run summary says how many and which), so the rest of the table keeps syncing while the rebuilt columns stop being overwritten. That lets you move one column at a time, check each, and delete the import once the source has nothing left to fill. Applies to every import source.
- Renaming a table: click the table’s name at the top of Table Settings, or the ✎ beside it in Workspace settings, and type the name you want — spaces, punctuation and capitals all allowed. This changes the name everywhere it is SHOWN, including what the AI assistant calls it; the table’s underlying database name never changes, so nothing built on it (sharing, folders, reports, computed columns, imports) can break. Clear the name to go back to the database one. The underlying name is always one click away on the ID chip beside the title. Admins only.
- Table Settings (the ⚙ on a table) → Column Settings: show or hide any column (including the Created date), drag to reorder, rename, lock, make a dropdown, or change a column's type. The header row stays frozen while you scroll.
- Computed columns: add a Calculation (numbers & IF logic, text tools, and date tools), Combine text (drag the pieces to reorder them; date columns show as MM/DD/YYYY, matching the grid), a Date calc, or a Per-key value (occurrence number, a first-row flag — 1 on each key's first row and 0 on the rest, so its total counts each key once — how many times the key appears, the key's total, or its highest / lowest; an occurrence number or first-row flag follows the sort you pick, and numbers kept as text sort as numbers, 9 before 10). A new per-key column shows blank, with a “Calculating…” note above the table, until its first calculation finishes (usually under a minute), so the table stays fast meanwhile. If one can't be calculated, it shows #N/A with the reason instead of slowing the table down, and it is tried again automatically after the next app update. Pick a type first; a name is only required to save. Example — grab 5 characters after a dash: MID([col], FIND('-', [col]) + 1, 5); a Monday-start payroll week: WEEKSTART([Date], 2). In the Calculation builder, type or paste the formula in the box at the top (it always shows the current formula); open “Build by clicking” to add columns/math/functions with buttons.
- Linked columns with match conditions: a Linked column (Table Settings → Column Settings → the column’s type → Linked) can narrow WHICH row of the other table it reads, not only match on an id. Under “Match only rows where”, each line reads left to right: a column of the OTHER table, a test (is, contains, is before, is on or before, is greater than…), and either a fixed value or a column on THIS row — for example “Effective Date is on or before this row’s Service Date”. Every line must hold. Then choose what happens when several rows still match: the latest by a column (the rate in force on the row’s date — the usual choice for any history or log), collect them all into one cell, or any one (only right when the other table has one row per id). Dates and numbers compare by value even when they are stored as text. When two rows still tie, the top row wins by default — the way a spreadsheet takes the first match (for an imported sheet, the top row follows the sheet’s own order, updated on every sync); choose “the lowest row ID” for a winner that never changes when rows are reordered. If one group of lines might find nothing, add another with “+ Add group”: groups are tried in order and the first that finds a row wins — the same as a spreadsheet’s chain of lookups inside IFERROR, kept in one column (a group with no lines means any row for the id, so it goes last). Editing an existing linked column works the same way — open it, change its lines or groups, and save; the assistant can propose that change for you too. Changing a stored column to Linked keeps the column itself — same name, so filters, reports and formulas that use it keep working: its old values are cleared and it fills with the lookup’s live value. This is how a lookup with criteria is rebuilt in Data as a live column that keeps itself up to date.
- Computed columns are resilient per-cell: a row whose formula can't produce a value shows blank (e.g. nothing to extract), and a row that genuinely can't compute shows #N/A — the rest of the column keeps working instead of the whole column erroring. Right-click any computed cell and choose “Computed details” to see how that column is defined (read-only).
- Formula syntax (Calculation columns): reference a column with [square brackets]. Put text in 'single quotes' — double quotes are rejected — and an empty string is '' (two single quotes). There is no true/false; use 1 and 0, e.g. IF([Flag] = 1, 'Yes', 'No'). Comparisons: =, <> (not equal), <, >, <=, >=. Dividing by zero gives a blank (not an error). Functions are case-insensitive. Logic & numbers: IF, ISBLANK, ISNUMBER, ISERROR, IFERROR, ISNULL, COALESCE, ROUND, ABS, MIN, MAX, AND, OR, NOT. ISNULL and COALESCE skip anything BLANK, not only a missing value — so COALESCE([A], [B], [C]) moves past a column holding '' and lands on the first one with something in it, the same idea of “blank” ISBLANK uses. Text: LEFT, RIGHT, MID, FIND, REPLACE, TRIM, UPPER, LOWER, LEN, CONCAT (join two or more pieces — a blank piece simply contributes nothing), and TEXTBEFORE([col], marker) / TEXTAFTER([col], marker) for everything before or after the first occurrence of a marker (blank when the marker isn’t there, so a row that lacks it passes through rather than erroring). Note + is arithmetic only and never joins text — use CONCAT. Because one calculation can now both split and rejoin, turning ‘Cohen, Sara’ into ‘Sara, Cohen’ is a single column: CONCAT(TEXTAFTER([Name], ', '), ', ', TEXTBEFORE([Name], ', ')). Dates: WEEKDAY, WEEKSTART, DATEADD([Date], days) to shift a date by whole days (negative goes back; plain arithmetic like [Date] - 7 does NOT work), DATEDIFF([Earlier], [Later]) for whole days between, TODAY(), YEAR([Date]) / MONTH([Date]) / DAY([Date]) for a date's parts and DATE(year, month, day) to build one (a date that doesn't exist is blank) — a table whose calculations use TODAY() is recalculated automatically each new day (UTC), with a line in its Activity, and DATETEXT([Date], 'MM/DD/YY') to write a date as text in a format you choose (YYYY/YY year, MM/M month, DD/D day; other characters kept) — use it when joining a date into a key or label, since CONCAT on its own writes year-month-day. A blank date stays blank in every date function (it never counts as 1900-01-01). Numbers: N([col]) reads a blank (or anything that isn’t a number) as 0 — a blank in + − × ÷ otherwise makes the whole result blank, so write N([Adjustment]) + [Hours] when a blank should count as 0. Counting across rows: COUNTIFS, SUMIFS([Total], …), MAXIFS([Col], …) and MINIFS([Col], …) look at the OTHER rows of the table — a bare [Key] means “rows with the same Key as this row”, and any other part is a test on those rows, e.g. COUNTIFS([Customer], [Order Date], [Amount] > 0) or SUMIFS([Hours], [Employee], [Status] = 'Approved'); no matching row gives 0. LOOKUP('Rates', {Rate}, {Person_ID} = [Person_ID], {Effective} <= [Visit_Date], ELSE({Person_ID} = [Person_ID]), LATEST({Effective})) brings one value from ANOTHER table into a calculation — {Column} is a column of that table, [Column] one of this row; tests are = <> < > <= >= (numbers compare as numbers, dates as dates), {Column} = '' means blank, CONTAINS({Column}, [Column]) — and the other way round, CONTAINS([Column], {Column}), when this row's value is the one that has to contain the other table's; NOT(…) around either asks for “does not contain”, and a blank matches nothing in any direction; ELSE(…) is a fallback tried only when nothing matched before it — add one ELSE(…) per extra tier to the SAME lookup (they are never nested, and each tier repeats every test it needs, including the key); LATEST / EARLIEST picks by a column, otherwise the table's first matching row wins; no match is blank. Use it when a lookup is part of a formula, e.g. IF(ISNUMBER([Override]), [Override], COALESCE(LOOKUP(…), 0)); a column that is only a lookup is a Linked column. A column using LOOKUP is stored and refreshed like a per-key column, and again whenever that table changes. SUMBEFORE([Hours], [Employee], ORDER([Created], [Id])) totals the rows that come BEFORE this one in that order for the same key — a running total that leaves out this row (and any row tied with it on every ORDER column), e.g. the hours already worked earlier in the week. ISFROZEN() is true when THIS row is frozen, so a formula can treat a frozen row differently — IF(ISFROZEN(), 'Locked', [Status]) — and a table where no row has ever been frozen simply reads false. COUNTBEFORE([Employee], ORDER([Created], [Id])) counts those earlier rows instead of totalling them — with “up to and including this row” it is the occurrence order 1, 2, 3… A column using them is kept up to date a moment after each change, like a Per-key column. Note MAX/MIN compare values ACROSS COLUMNS in the same row (a blank is skipped); for the highest or lowest across the ROWS of a key, use MAXIFS / MINIFS or a Per-key column. In COUNTIFS / SUMIFS / MAXIFS / MINIFS / SUMBEFORE a key can be a column or a value of one, such as WEEKSTART([Date]) — SUMIFS([Hours], [Employee], WEEKSTART([Date])) totals the same employee's hours in the same week (Sunday to Saturday). ERRORS: a lookup that matches nothing shows #NO MATCH instead of a blank, which is what the source sheet shows for the same formula when nothing catches it — so “there was nothing to look up” and “I looked and found nothing” stop reading the same. A calculation that reads a column holding an error shows #BLOCKED (error in …), naming the column that blocked it. To handle an error, wrap it: IFERROR([Some Lookup], 0) uses 0 whenever that column holds an error, and ISERROR([Some Lookup]) is true when it does — IF(ISERROR([Some Lookup]), 'not on file', [Some Lookup]) writes your own wording. Testing a column with ISBLANK counts as handling it too, because ISBLANK reads an error as blank. Naming a column inside IFERROR, ISERROR or ISBLANK tells the calculation you are dealing with it, so the whole formula keeps working instead of showing #BLOCKED, including where that column is used again later in the same formula. A column you DON'T wrap still blocks, which is the point: a total computed from an error is not a total. You can also give the lookup a stand-in — COALESCE(LOOKUP(…), 0) shows 0 where nothing matched — and inside the same formula ISBLANK still treats its own lookup's no-match as blank. MATCHPOS('Table', {Key} = [Key]) gives the POSITION of the first matching row in that table's own recorded row order — the number a sheet's MATCH(value, {range}, 0) returns — and #NO MATCH when nothing matches, so a sheet's IFERROR(…, 0) becomes COALESCE(MATCHPOS(…), '0'). When SEVERAL sheets are imported into one table, say which rows are being counted: WITHIN({Source} = 'the sheet') numbers that sheet's rows from 1 as the sheet itself does — without it the position is a place in the whole pile, which appears on no sheet. WITHIN compares a {Column} with a fixed value only. Use MATCHPOS for PARITY with a source row-number column; never build anything on the position, because it moves whenever the source is re-imported in a different order. A lookup COLUMN rebuilt from a source formula that had no fallback shows #NO MATCH in the same way; a lookup you build yourself, and one that replaces an existing column in place, show a blank instead. A CHECKBOX from an import arrives as the word “true”, and comparing it with 1 or 0 does what you mean: [Offsite] = 1 is ticked, [Offsite] = 0 is not, while a column holding a number still compares as a number.
- AND / OR work both ways — as words between conditions, IF([A] > 0 AND [B] > 0, …), or as functions, IF(AND([A] > 0, [B] > 0), …); NOT([A] = 1) negates one. Numbers stored as text still do math: a value like "$1,234.56" is read as a number, and anything that isn't a number comes out blank instead of erroring.
- Not sure how to write a formula? Open the AI chat (the assistant in the Database & Reports app), describe what you want in plain English — "5 characters after the dash", "blank when both columns are empty, otherwise add them" — and it will draft the formula for you and (as an admin) offer to add the column.
- Linked columns: pull a column in from another table by matching an id — the match key can be a Combine/computed column (not just a stored one, so tables with no shared id can still be joined), and you can add an optional condition (‘only pull the value when [this table’s column] contains / is / … X’) for a conditional IF-then lookup — the cell stays blank when the condition isn’t met.
- Open a report to see data filtered by its saved columns/values/sort; edit the report or its columns from the header.
- Open a dashboard (admin-created) to see widgets.
- Use New + to create a Table, Report, Dashboard (admin), Workspace, or Folder, or to Import.
- Share any table, report, or workspace with specific people/roles using the Share (⇆) buttons.
- Filters: click Filter for a LIST of your filters, each with an on/off switch — one is on at a time, because the grid shows one set of rows. Besides any named filters you get exactly ONE unsaved filter per table, for a one-off question; building another unsaved one replaces it, and you can name it later to keep it. Edit opens that filter in place under its own row, and Save changes lights up only once something actually differs, so opening a filter to look at it changes nothing. Operators: contains / does not contain, equals / does not equal, is blank / is not blank, is a date / is not a date, is one of / is not one of, plus the date operators (on / before / after / between / in the last N days) on date columns. Any column can be compared to ANOTHER COLUMN ('equals another column' / 'does not equal another column'), which is how you spot rows where two values that should agree don't. Any column also offers 'appears more than once' / 'appears only once', which is how you find duplicates: filter a key column to the values that repeat, and only the rows sharing a value are shown. It asks the whole table, not the rows currently on screen, and ignores blanks so empty cells never read as duplicates of each other. 'is a date' is offered on non-date columns — it is for a text column that holds dates among headings and 'n/a's; a blank counts as not a date. A report's OWN filters use the same builder — groups, ALL/ANY within a group and across groups, and the same operators — so a report can be defined as “A and (B or C)” rather than one flat all-must-match list. A report can also be limited to the signed-in user by matching their email against a column; that applies across the whole report rather than sitting inside a group. Automations offer the same operators. An admin can share a saved filter with the whole team (others can switch it on; only an admin can edit or remove it).
- Multi-cell copy: click-and-drag to select a rectangle of cells across rows/columns, then Ctrl/Cmd+C to copy them as tab-separated values that paste straight into Excel or Sheets. (There's no in-grid paste — edit through the row drawer.)
- Attachments: hover a row and click the 📎 to attach files to that row (preview / download / remove in the drawer); a paperclip with a count shows on rows that have files. Files are stored in your own SharePoint/OneDrive and open in-app. An admin sets this up in the hub under Settings → Org Settings → OneDrive / SharePoint; until then the drawer prompts to add it.
- Comments: hover a row and click 💬 (or the floating discussion button) to comment on a table or a specific row; @mention teammates to notify them. You can hide the per-row action icons per table in Table Settings.
- "Setting up" / #N/A: a table's computed values are pre-stored so the grid loads fast. Right after you add or change a computed column (or a table it looks up from), the store rebuilds in the background — a filter on it may briefly say it's "being set up" and cells may show #N/A until the rebuild finishes (usually under a minute; longer for very large tables). It clears on its own.
- Checking calculated columns: Table Settings, Column Settings has a 'Check' button beside 'Refresh now'. Check AUDITS the stored values: it works out what each column should hold, compares that with what is stored, and reports any differences without changing anything. A difference is not automatically wrong (a row edited seconds ago is briefly ahead of its rebuild); one that persists after a refresh means a formula's dependencies need a look. 'Refresh now' is only for data changed OUTSIDE AppFrunk (another tool writing to the database) - ordinary edits, imports and formula changes already schedule their own recompute.
- Column Settings marks where a column's values come from and who depends on them: a column an auto-import fills shows a ↻ marker naming the flow and saying whether its row handling overwrites your edits (an update or replace-all sync replaces what you type on its next run; an add-new sync does not); a column other tables link to shows ↗ used by N, listing them — renaming carries across automatically, and deleting asks first and names what would break. The tracking columns an import maintains itself are locked with no unlock control, because editing one breaks the sync that matches on it. The grid’s Export downloads every row that matches your current filters and sort as a CSV (no row limit), showing its progress as it goes. Other Table-Settings tabs: Formatting (conditional colour rules), Automations, Forms, Exports, Snapshots (restore a backup of the table's rows), and Activity. FORMS are public/intake forms that write into the table — pick which columns appear from the Fields dropdown (searchable, tick a column to put it on the form, untick to take it off), and add signature pads and dividers alongside them. EXPORTS run on a cadence and upload a CSV of every row — calculated and lookup columns included, with no row limit — to your SharePoint; the time is set and shown in YOUR timezone and stays at that clock time through daylight saving, and saving a schedule creates the SharePoint folders and links them so you can open the destination straight away. ACTIVITY is a filterable log of changes — by date, by person, or by ACTIVITY TYPE (cell edits, rows added, rows deleted, reverts, imports, structure changes, recalculations, and who opened the table). Rows added by one import appear as a single 'N rows added' entry naming the source, with the flow and run behind View run — they are recorded one per row so each row's own Cell history can say who created it. A recalculation shows how far a formula change rippled, with the per-column breakdown behind View change.
- Summary fields (Table Settings › Summary) and the Σ bar under the grid: calculate over the sheet — Sum, Average, Min, Max, Count, Count filled. Pick the column from a searchable list, and optionally give a field CONDITIONS (the same operators as a filter) so it counts only some rows: 'total for this week', 'how many are still open'. Conditions are per FIELD, so two fields can ask different questions; none means the whole sheet. A filtered grid narrows the summary too, and both work on calculated columns as well as ordinary ones.
- Automations: run when a cell changes, a row is added, a date is reached, or on a schedule - then send an email or in-app alert (to one person, or to everyone named in a contact column), set or clear a value, assign someone, or copy/move rows. Conditions use the same operators as the filters, including comparing one column against another. A saved rule can be edited in place, run on demand (the play button skips the trigger but still applies the conditions), and has its own activity log showing what each run did and why.
- In Browse, choose New + → Import.
- Manual Import: drag in a CSV, TSV, or Excel (.xlsx) file. Pick an existing table or create a new one.
- Finding and filing imports: the Imports page has a search box (by name) and filters for destination table, mode and status (Active, Paused, or Failed last run). Imports can be filed into folders — type a folder name when creating or editing one (an existing name files it there, a new name creates the folder), or drag an import onto a folder heading. Headings collapse, rename, and reorder by dragging or with the up/down arrows; removing a folder moves its imports back to the top level and deletes nothing.
- Multi-tab Excel: set the Tab number (left-to-right, 1 = the first tab) to choose which sheet to import. Set a Header row number when the real header isn't the first row — everything above it is skipped, that row becomes the header, and rows below it are the data. Both apply to manual and auto-upload imports (Excel, CSV, and TSV).
- Stack all tabs (manual OR auto-upload): for a workbook with one tab per entity, turn on "Stack all tabs into one table" to read EVERY tab and append the rows into one table — overriding the Tab number. Turn on "Include the tab name on each row" (on by default) to create a column (defaults to Source Tab Name) where each row carries the name of the tab it came from; it's locked read-only like the Created date, but you can hide/rename it and use it in computed columns. The Header row number applies to every tab, and a column that appears on only some tabs is still included.
- Choose row handling: Add new, Update + add missing, Update + skip missing, or Update + replace all (mirror). If updating, pick an ID column and a match mode.
- Source ID column: next to the ID column you can name the column IN YOUR FILE that supplies it. Leave it on “Same as ID column” and nothing changes. Set it and that pairing is pinned ahead of the column mapping, so the file’s column can be named differently, and later edits to the mapping cannot change what identifies a row - the usual cause of a re-sync adding rows it should have recognised.
- ID checks: every import that matches on an ID column counts the rows with no ID and the IDs given to more than one row, and names them in the run summary (only one row per ID is kept). An Update + replace all (mirror) import stops without changing anything when more than 5% of rows have a missing or repeated ID, because it deletes rows by that ID. Sheet imports match on each row's own row ID by default, which never repeats and is the recommended setting; pick your own ID column only when rows get re-created in the sheet (for example a scheduled job replaces them), which gives them new row IDs.
- Map columns (manual OR auto-upload, existing tables): turn on "Map columns to specific table columns" to route each source column into the table column you pick from a dropdown — or set it to Ignore to drop it. This lets a differently-named file load into a shared table without renaming anything, and drops junk columns. Each destination is used once. For any column not listed, choose whether to match a same-named table column or ignore it. On an auto-upload flow there's no file at setup, so get the source columns from a sample file, a file already in the watch folder, or by typing the names in — in every case only the headers are read, nothing is imported; the map is saved and applied to every future file.
- Auto Upload: watch the shared AppFrunk/Data folder (or a subfolder) so files dropped there import automatically — great for feeding data from the Sheets app's exports.
- Only import SOME rows: turn on 'Only import some rows' to skip (or keep) rows by what they contain — contains / does not contain / is exactly / is not / is one of / is not one of (comma-separated) / is blank / is not blank / is a date / is not a date / is a number / is not a number, joined by ALL or ANY. Made for the junk a real export carries: subtotal lines, a trailing Total, blank separators, repeated headers, rows for another location. Rules are written against YOUR FILE's column names and applied before mapping, so a filtered-out row never reaches the table. 'is one of' takes a comma-separated list and matches each value exactly (case-insensitive), so codes that share a prefix can't collide. 'is a date' counts a DATETIME as a date — a cell reading 08/28/2025 4:00PM is a date — and the cell is read exactly as the spreadsheet displays it; a bare number such as 2024 is not counted as a date. Column names come from the same sample-file / file-in-the-folder / type-them-in control the column map uses, so you can set this up before any file exists.
- Re-importing the same source into a table with NO ID column: by default rows are matched by comparing their whole contents, so a row whose values changed looks new and is added again. Changing the column mapping does NOT do this - a row is identified by the source row it came from, not by the columns you chose to keep, so mapping an extra column fills it in on the rows you already have. Turn on 'Recognise rows from this source again' and each imported row is stamped with a fingerprint of where it came from, so a later import UPDATES those rows instead of duplicating them. 'By row content' suits any file; 'by position in the file' also follows a row whose values changed, but only when the same file is replaced and its row order never changes. The first sync after switching it on only REPORTS what it matches and imports nothing, so you can check the numbers before letting it load for real.
- Smartsheet sync: a Smartsheet source can apply the SHEET's column types to the destination after each sync (a date stays a date, a picklist becomes a dropdown, a contact list a Contact column). It only types columns still set to plain text, so a type you chose in Data is never overwritten.
- Manage each auto-upload flow: Edit, Sync now, Pause/Resume, or Delete. Activity shows THAT flow's runs and what each one added, updated and skipped. A dropped file imports within seconds; a sleeping serverless database simply imports on the next run once it wakes. If a flow's last run failed, it shows a red ✗ Failed in Status — hover it for the reason and when it is tried again. When a database error caused it, a second, smaller line under the reason (in the flow's Activity and the table's Activity log) gives the database's error number, class and state, the step that failed (for example adding new columns, staging rows, removing duplicates or merging into the table) and the error's own text with any values removed; a sheet read that fails gives the sheet service's error code, HTTP status and reference id. A failing flow is retried automatically after 1, 2, 4, 8, 16 and 30 minutes, then 1, 2 and 4 hours, then every 6 hours — edits to the sheet wait for that retry too — so a source that is down isn't hammered and a sleeping database isn't kept awake; Sync tries it right away, and the ✗ clears on the next run that works. Each failure is also written once to the table's Activity log (type Failed imports) with its reason. Sheet imports check for changes every minute with the sheet's version number, and read a changed sheet 1,000 rows at a time. Each sheet on a sheet import shows its sheet id, and you can add a sheet by id (to change one, remove it and add the right id). Ids are checked when you save: a wrong id, a sheet that isn't shared with the account whose API key the import uses, or one that account isn't allowed to read is refused with the reason. If a sheet stops being readable later, the import's ✗ says which sheet and whether it is a wrong id / not shared or no permission. An import that matches rows on an ID also has a required When an ID is on more than one row setting: keep the first row of each repeated ID (the default), keep the last, leave every row of that ID out, or stop the sync. Whichever you pick, everything else still syncs and the run summary says how many IDs repeated and how many rows were left out; to keep every row of a sheet, match on the sheet's own Row ID instead.
- Open Data check (from Browse or SQL Settings → Health). Editor/Admin only, read-only.
- Compare tables: pick a source and target table plus ID column(s) and an optional total column; see counts, rows missing/extra, and changed values on matched keys.
- Find duplicates: one table or across several, on one or more columns, optionally case-insensitive.
- Recent ingest failures: 'Check now' lists failed/blocked/quarantined loads with error messages.
- This is almost always your database itself, not AppFrunk — the app just connects to it. There are three common causes.
- 1) Serverless auto-pause: an Azure SQL serverless database pauses when idle and takes up to a minute to resume on the next connection. AppFrunk retries automatically, so it should load without a refresh.
- 2) Free-tier monthly limit reached: the Azure SQL free offer includes a limited amount of compute per month (about 100,000 vCore-seconds ≈ ~55 hours of always-on time). If it’s used up, the database is set to pause until the 1st of next month and will NOT resume on connection — that shows as endless loading.
- 3) An Azure platform health event: occasionally Azure itself has trouble resuming a database and raises a “Health Event” on it, so the resume gets stuck. This is entirely on Azure’s side and usually clears on its own within an hour.
- HOW TO CHECK whether it’s an Azure-side problem (not your credentials, firewall, or AppFrunk): (a) Resource health — in the Azure portal open your SQL database → left menu under “Help” → “Resource health”: “Available” = healthy, “Degraded/Unavailable” = Azure’s side. (b) Activity log — same database → “Activity log”: a “Health Event Activated (Active)” or a stuck “Resume Databases” confirms it. (c) Query editor (preview) → run SELECT 1: if even that returns “Database … is not currently available,” it’s 100% Azure’s side. (d) Region-wide: search “Service Health” in the portal, or see status.azure.com.
- When Resource health shows unavailable or a health event is active, there’s nothing to change on your end — wait for it to clear, reach @AzureSupport on X, or post on the Microsoft Q&A community. Note: an Azure TECHNICAL support ticket needs a paid support plan; a stuck-resume has no free technical-ticket path, but it almost always self-resolves.
- What uses up the free compute: anything that keeps the database awake, because a serverless database only pauses after it has been fully idle for its auto-pause window. Leaving a table open in a browser tab, frequent auto-imports, or other tools (SSMS/Object Explorer, Power Automate) polling it all reset that timer.
- Keep running past the free limit (recommended): in the Azure portal open your database → Compute + storage → set “Behavior when free offer limit reached” to “Continue using the database for additional charges” — small serverless pay-as-you-go (roughly $0.000145/vCore-second beyond the free amount, a few dollars a month at light usage); the free amount still renews on the 1st.
- Keep it ALWAYS ON (no wake-up wait at all): either turn OFF auto-pause (Compute + storage → auto-pause delay = Never — you then pay for it running continuously) or move to the Provisioned compute tier instead of Serverless. Provisioned is the better fit for a production database used all day; Serverless with auto-pause is cheapest for intermittent use where an occasional ~1-minute wake-up is acceptable. Microsoft’s guide: learn.microsoft.com/azure/azure-sql/database/serverless-tier-overview.
- Reduce compute use on the free/serverless plan: close tables/tabs when you’re done, and pause auto-imports you don’t need.
- Open AI Chat. Ask questions in plain English — it reads your live tables to answer (uses AI credits).
- It can propose changes — edit a cell, insert a row, a bulk edit of every row matching a filter, a saved (or admin-shared) filter, a new report, a backup snapshot, a comment, and (for admins) structure changes like adding a column/computed formula or a new table — nothing is written until you approve it — one at a time, or Approve all, which applies the waiting ones in order (a failed one shows its reason and a Retry; Deny is always there). “X of Y approved” under the cards shows where you are, and the cards with their results, the task list, and anything you typed but haven’t sent all stay put when you reload the page. For safety it never DELETES anything (rows, tables, reports, dashboards are manual-only); it can explain how to do that yourself.
- Bigger jobs become a TASK LIST (the assistant can also mark a step done, skip it or reopen it when you ask — the same as the buttons): the assistant lays out the steps as a checklist (an approved import step shows running for everyone right away and turns done or failed when its sync ends, even if you close the page) and you Approve each — or Approve all — with steps running in order (each waits on the ones it depends on); Skip, Remove (takes that one step off the list), Retry, and Clear are there too. Nothing runs until you approve.
- Admins can ask it to migrate Smartsheets into Data: it reads your Smartsheet structure and formulas (never your row data), then builds a task list that recreates it here the best way — in-sheet formulas become computed columns, cross-sheet references become linked columns, similar sheets combine into one table. For each source sheet it asks once: one-time import, or keep it linked (auto-syncing). It never changes anything in Smartsheet. Each formula is translated by a built-in translator, not guessed: a lookup with criteria, a “latest by date” pick and a chain of fallbacks become one linked column that keeps the column’s name; an in-row formula becomes a calculation; anything it can’t express is listed for you to decide. When a formula reads one sheet of several that import into the same table, the rebuilt lookup only matches rows from that sheet (using the import's source-sheet name column). A column that is a lookup in Data can be changed into the calculation its sheet formula translates to, once you decide so: one card replaces the lookup with the calculation under the same name (if the calculation can't be added, the lookup is put back). Its recalculation line in Activity then says the column was calculated for N rows and replaces the lookup (or stored column) of the same name, rather than counting those cells as added. To check a change without reading rows, ask the assistant to count rows where a test holds — for example how many rows' new rate differs from the old rule — and it answers with counts only; a test that names a particular value is refused. It can also group and total rows (for example sessions per code, or how many have no gross pay), narrowed by simple filters or a list of row ids. To explain how a value comes about, it reads the formula of the column that decides it and follows the columns that formula uses. When stored calculated or lookup values look out of date, it can propose a Recalculate card: approving it recalculates the table in the background with every formula unchanged, and the table's Activity shows Recalculated on request with who asked. An admin's assistant can read any part of the Smartsheet API it needs (sheets, reports, dashboards, workspaces, folders, search), retries a read that fails for a moment, and when an id isn't found as the kind it was given (a sheet id used as a workspace) tries the other kinds it could be. Asked whether a sheet is in use, it can give each sheet's row count, version and last-changed date. For an API key limited to metadata, row values, comments and attachments are removed from what it reads.
- Attach or paste screenshots (up to 6 images) to give it context.
- AI agent API (admins): Settings › API keys creates a key a program can use to talk to the same assistant — it sends a message exactly as if the key's owner typed it and gets back the reply and any proposal cards; nothing changes until a card is approved. A key can be allowed to apply an approved card (calculated, per-key and lookup columns, dropdowns, adding/renaming/retyping a column, reports, saved filters, a single import, filing a table into a workspace, clearing a task list, running one of its own task lists' single-sheet import steps — task-list steps, deleting columns and row edits stay in the app). The assistant can do what the Imports page does for sheet imports: find imports by table, folder, name or status (in pages, and it says when a list is partial), show an import's run history and the Import progress list, change any setting, rename, pause or resume, sync, move into, rename, remove or reorder folders, clear finished jobs, and propose deleting an import (a person approves a delete in the app; the table and its rows stay). It can also list every card still waiting for approval, check every sheet in a Smartsheet workspace or folder against the auto-imports in one go (which sheets have an import, which import, into which table and workspace, running or paused, last run, failures, and imports whose sheet is gone), and check every table in a Data workspace for the imports that load it. When a name matches several sheets, imports, folders or workspaces, it asks you which one instead of picking. An import card names the import (after its destination table unless you give a name), the workspace a new table goes in and the Imports-page folder it is filed in; the assistant can also propose renaming an existing import — either can be one that exists, or a new one only when you asked for it by name or said yes when the assistant asked — it never creates one on its own, and asks you when a name is unclear or close to one that exists — and what a sync does with an id repeated in the source (keep the first by default); the assistant can list workspaces and propose clearing a whole task list (a card you approve; a cleared list can be restored), and refuses one card that stacks two differently-shaped sheets or a sheet already on another import card. A key without "Read row data" is metadata-only: the assistant can't read row values and personal details are removed from its replies. "Compute on data (results only)" sits in between: the assistant may read any data to check, count and total for that key, and every reply passes a patient-information filter before the key receives it — names, dates of birth, record numbers and contact details are removed, staff names, codes, counts and totals are kept — and a reply the filter can't check is withheld rather than sent. Keys act as the admin who created them (never beyond that person's current access), can expire, can be rotated (the old key works 15 more minutes) or revoked, and an internal automation key lives only in Key Vault and rotates daily. An organization can have several internal automation keys — one per agent, each with its own memory, its own conversation and its own Key Vault secret named after the key (shown under the key). A key's name, access (apply changes / read row data) and expiry can be changed after it is created with Edit (an internal key keeps its secret, so a working agent isn't cut off; turning on row data asks first), a revoked key can be deleted for good after an Are-you-sure, and every create, change, rotation, revocation and deletion shows in Settings › Audit with the key's name and, for a change, the setting's old and new value. In Agent view, the Agent picker chooses whose conversation to show and remembers the choice. Task lists are per agent too: a task list an agent's key creates is that agent's alone — its key sees and changes only its own task lists (not the app's, not another agent's), while people in the app see every task list, each agent's marked with the agent's name. Use is billed to your AI credits. On the AI data assistant page, the Agent view switch (off by default; available while an active API key exists) shows a key's conversation with the assistant — its messages, the replies and each proposal's status — in the same chat layout. An admin can Approve or Deny the agent's waiting cards right there (the change is made as that admin), and each card shows where it was decided: in the app and by whom, or through the API. The agent is told what became of its cards on its next message, and the assistant can list a table's recent structure and import changes — who made each, in the app, through the API or by an import — so it knows what is already done. It follows the agent live: a new message shows at once and its reply as soon as it is saved, with no reload. (Settings, with all its tabs, is available to admins only — anyone else is asked to contact their account admin.) Details: the API docs page.
- Each reply has a Copy button at its bottom: it copies the reply as it stands — its text, what it changed in the task list, the task list itself, and each change it proposed with its result (applied, denied, awaiting approval) — so you can paste it anywhere. Under a reply that touches a task list, a small box shows exactly what that reply changed in it, taken from the app itself rather than the assistant’s own summary — “None” means nothing was changed. Proposal cards sit at the bottom of the reply, and approving one no longer jumps the chat.
- Use the memory panels to review today's conversation or earlier days, and Clear them when you want.
- Settings → Your account: change your display name, email, or password (email/password changes require your current password).
- Team access and billing are managed on the AppFrunk hub; sharing of specific tables/workspaces is done with the Share buttons in Browse.
System status: status.appfrunk.com