> ## Documentation Index
> Fetch the complete documentation index at: https://docs.anyreach.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Keeping data fresh

> Point a workflow at your source system so the table refreshes itself.

Importing by hand is fine for data that changes rarely. For anything that moves — a price list, a store roster, stock levels — build a workflow that reads your source and applies it on a schedule.

Nothing about Custom Data knows where your data came from. A Google Sheet, a partner API, an internal database, a nightly export: all the same to the table.

## The shape of a sync

<Steps>
  <Step title="Trigger">
    A schedule. Every 30 seconds for stock levels, nightly for a price list — whatever matches how fast your source actually changes.
  </Step>

  <Step title="Read the source">
    A **Google Sheets** action, an **HTTP** step against an API, or any other connector.
  </Step>

  <Step title="Shape the rows">
    A **Transform** step to map the source's fields onto your declared column names.
  </Step>

  <Step title="Apply">
    A **Custom Data → Replace Table** action.
  </Step>
</Steps>

## Passing a dynamic number of rows

On the Replace Table action, the **Rows** field has two modes. **Items** takes rows you type in — fine for a fixed handful. For a sync you want **One expression**, which takes a single expression evaluating to the whole array:

```
{{ n5.return_value.records }}
```

That's the mode to use whenever the row count isn't known until the workflow runs — which is almost always.

## Replace, not Add

Use **Replace Table** for a mirror. It's the only operation that propagates deletion: if a product vanishes from your source, it vanishes here, because absence is the signal.

**Write Rows** (add-or-update) never deletes, so a mirror built on it accumulates rows your source dropped months ago.

<Warning>
  Replace deletes anything absent from the payload. If your source read fails halfway and returns 12 rows instead of 900, that's a request to delete 888 rows.
</Warning>

Rows are tombstoned rather than erased, so the next successful run restores everything — but until then your agents can't see them. If your source can return partial data, put a **Condition** step before the apply that fails the run when the row count looks wrong for your data. The platform can't tell a legitimate shrink from a broken read; you can.

## Running often is cheap

A refresh where nothing changed writes nothing at all. Rows are compared to what's stored, and identical ones are skipped entirely — no timestamps move, no storage churns.

That has a useful consequence: **the "last changed" time on your table means when the data actually changed**, not when the sync last ran. A table showing no change for three days with a healthy workflow means your source has been stable, not that something is broken.

You can therefore poll far more often than you'd expect. Every 30 seconds against a small source is unremarkable.

## Watching it

The workflow's own run history tells you whether the sync **ran**. The table tells you whether the data **changed**. They're different questions and you want both:

* Workflow keeps failing → your source or credentials are broken
* Workflow succeeds, `written: 0` every time → the source is stable, or you're reading a stale cache
* Guard refusals → your source is returning partial data

Put a **Condition** step after the apply to alert on unexpected results — a replace reporting more deletions than usual is worth knowing about before your agents start telling callers a product doesn't exist.

## More than one writer

Nothing stops two workflows writing to one table, but one writer per table is the sane arrangement. Two mirrors of different sources will fight, each deleting what the other just wrote, and the table will flip between them on every tick.

If you need data from two systems in one table, join them in the workflow — read both, merge, apply once.

## Where the row key comes from

Your source's own identifier. The whole diff depends on it: a stable key means a refresh is a small update, and an unstable one means every refresh replaces your entire table with itself.

If your source has no stable identifier, build one in the Transform step from fields that don't change — and make sure it's genuinely unique, or the import will refuse it as duplicate.
