Yesterday's seen = set() lives in memory. Script ends → set vanishes. Re-run → empty set → all items look new → duplicates.
Real automations survive script ending. The fix: store state somewhere that persists. For Patterns, the state store is a Google Sheet — a row per processed key. Today's pattern:
seen set.seen, do the work, append a row with the key.# Step 1: discover a spreadsheet
found = toolset.execute_action(Action.GOOGLESHEETS_SEARCH_SPREADSHEETS, {"query": ""})
spreadsheet_id = (found.get("files") or found.get("response_data", {}).get("files", []))[0]["id"]
# Step 2: append a marker row
result = toolset.execute_action(Action.GOOGLESHEETS_SHEET_APPEND_GOOGLE_SHEET_ROW, {
"spreadsheet_id": spreadsheet_id,
"sheet_name": "Sheet1",
"values": [["zuzu-day-12-marker"]],
})
# Step 3: confirm the append
updated_range = (result.get("updates") or result.get("response_data", {}).get("updates", {})).get("updatedRange")
print(f"appended to: {updated_range}")The Sheet is now state. Next time this script runs, it could read rows back and skip the marker.
Where does the spreadsheet come from?
You need at least one spreadsheet in your Drive. The SEARCH_SPREADSHEETS action finds it. Real automations usually create-or-find a specific "state" spreadsheet by name — for today's lesson, taking the first match is fine.
Could I use Notion or a database instead?
Yes — anything you can read and write becomes a state store. Sheets is convenient because it's already in your Drive, easy to inspect manually, and Composio supports both append and read. Tomorrow's lesson uses the same Sheet to count partial failures.
| Scope | Survives? | When |
|---|---|---|
| Local variable | no | Vanishes when function returns |
| Module-level variable | no | Vanishes when script ends |
| File on disk | yes (until restart wipes the sandbox) | Survives a single machine run |
| Sheet / Notion / DB | yes | Survives anywhere — different machine, different day |
The sandbox a Patterns lesson runs in is fresh per submission. Nothing on its disk survives. State has to live in a tool.
# PHASE 1 — read state
rows = toolset.execute_action(Action.GOOGLESHEETS_BATCH_GET, {
"spreadsheet_id": ID, "ranges": ["Sheet1!A:A"]
})
seen = {r[0] for r in rows.get("valueRanges", [{}])[0].get("values", []) if r}
# PHASE 2 — process new items, append each as you go
for item in items:
if item in seen:
continue
do_work(item)
append_row(ID, [item])
seen.add(item)Phase 1 builds the seen set from durable storage. Phase 2 mutates the durable storage as it works.
If the script crashes between phase 1's read and a single end-of-loop write, you've done work but haven't recorded it — next run duplicates. Append per item. A crash mid-loop loses only the in-flight item, not all completed work.
The simplest schema: one column.
A
├── id-1
├── id-2
├── id-3More informative: id, timestamp, status.
A B C
id processed_at status
id-1 2026-05-04T09:00:00Z ok
id-2 2026-05-04T09:00:01Z failedWhich you choose depends on what you'll grep for later.
found = toolset.execute_action(Action.GOOGLESHEETS_SEARCH_SPREADSHEETS, {"query": ""})
files = found.get("files") or found.get("response_data", {}).get("files", [])
spreadsheet_id = files[0]["id"]For today's minimum demo: take the first spreadsheet found. Real scripts filter by name (query="name='zuzu-state'") — but the discovery shape is the same.
Composio wraps Sheets responses inconsistently:
# Append response
updated = (result.get("updates")
or result.get("response_data", {}).get("updates", {})).get("updatedRange")Read both paths. The lesson asserts on whichever exists.
Yesterday's seen = set() lives in memory. Script ends → set vanishes. Re-run → empty set → all items look new → duplicates.
Real automations survive script ending. The fix: store state somewhere that persists. For Patterns, the state store is a Google Sheet — a row per processed key. Today's pattern:
seen set.seen, do the work, append a row with the key.# Step 1: discover a spreadsheet
found = toolset.execute_action(Action.GOOGLESHEETS_SEARCH_SPREADSHEETS, {"query": ""})
spreadsheet_id = (found.get("files") or found.get("response_data", {}).get("files", []))[0]["id"]
# Step 2: append a marker row
result = toolset.execute_action(Action.GOOGLESHEETS_SHEET_APPEND_GOOGLE_SHEET_ROW, {
"spreadsheet_id": spreadsheet_id,
"sheet_name": "Sheet1",
"values": [["zuzu-day-12-marker"]],
})
# Step 3: confirm the append
updated_range = (result.get("updates") or result.get("response_data", {}).get("updates", {})).get("updatedRange")
print(f"appended to: {updated_range}")The Sheet is now state. Next time this script runs, it could read rows back and skip the marker.
Where does the spreadsheet come from?
You need at least one spreadsheet in your Drive. The SEARCH_SPREADSHEETS action finds it. Real automations usually create-or-find a specific "state" spreadsheet by name — for today's lesson, taking the first match is fine.
Could I use Notion or a database instead?
Yes — anything you can read and write becomes a state store. Sheets is convenient because it's already in your Drive, easy to inspect manually, and Composio supports both append and read. Tomorrow's lesson uses the same Sheet to count partial failures.
| Scope | Survives? | When |
|---|---|---|
| Local variable | no | Vanishes when function returns |
| Module-level variable | no | Vanishes when script ends |
| File on disk | yes (until restart wipes the sandbox) | Survives a single machine run |
| Sheet / Notion / DB | yes | Survives anywhere — different machine, different day |
The sandbox a Patterns lesson runs in is fresh per submission. Nothing on its disk survives. State has to live in a tool.
# PHASE 1 — read state
rows = toolset.execute_action(Action.GOOGLESHEETS_BATCH_GET, {
"spreadsheet_id": ID, "ranges": ["Sheet1!A:A"]
})
seen = {r[0] for r in rows.get("valueRanges", [{}])[0].get("values", []) if r}
# PHASE 2 — process new items, append each as you go
for item in items:
if item in seen:
continue
do_work(item)
append_row(ID, [item])
seen.add(item)Phase 1 builds the seen set from durable storage. Phase 2 mutates the durable storage as it works.
If the script crashes between phase 1's read and a single end-of-loop write, you've done work but haven't recorded it — next run duplicates. Append per item. A crash mid-loop loses only the in-flight item, not all completed work.
The simplest schema: one column.
A
├── id-1
├── id-2
├── id-3More informative: id, timestamp, status.
A B C
id processed_at status
id-1 2026-05-04T09:00:00Z ok
id-2 2026-05-04T09:00:01Z failedWhich you choose depends on what you'll grep for later.
found = toolset.execute_action(Action.GOOGLESHEETS_SEARCH_SPREADSHEETS, {"query": ""})
files = found.get("files") or found.get("response_data", {}).get("files", [])
spreadsheet_id = files[0]["id"]For today's minimum demo: take the first spreadsheet found. Real scripts filter by name (query="name='zuzu-state'") — but the discovery shape is the same.
Composio wraps Sheets responses inconsistently:
# Append response
updated = (result.get("updates")
or result.get("response_data", {}).get("updates", {})).get("updatedRange")Read both paths. The lesson asserts on whichever exists.
Create a free account to get started. Paid plans unlock all tracks.