A script that talks to two tools — Tasks and Calendar — quickly accumulates boilerplate. Each tool has its discovery step, its response-shape quirks, its result.get(...) defensive reads.
The ugly version:
# add a task
lists = toolset.execute_action(Action.GOOGLETASKS_LIST_TASK_LISTS, {})
items = lists.get("items") or lists.get("response_data", {}).get("items", [])
toolset.execute_action(Action.GOOGLETASKS_CREATE_TASK, {
"tasklist_id": items[0]["id"],
"title": "task A",
})
# add a calendar event
toolset.execute_action(Action.GOOGLECALENDAR_CREATE_EVENT, {
"calendar_id": "primary",
"summary": "event A",
"start_datetime": "2026-12-31T10:00:00+00:00",
"event_duration_minutes": 30,
})The helper version:
def add_task(title):
lists = toolset.execute_action(Action.GOOGLETASKS_LIST_TASK_LISTS, {})
items = lists.get("items") or lists.get("response_data", {}).get("items", [])
return toolset.execute_action(Action.GOOGLETASKS_CREATE_TASK, {
"tasklist_id": items[0]["id"],
"title": title,
})
def add_event(summary):
return toolset.execute_action(Action.GOOGLECALENDAR_CREATE_EVENT, {
"calendar_id": "primary",
"summary": summary,
"start_datetime": "2026-12-31T10:00:00+00:00",
"event_duration_minutes": 30,
})
# orchestration reads cleanly
add_task("task A")
add_event("event A")The main flow names what it's doing. The plumbing is in the helpers.
When do I extract a helper?
When you'd write the same lines twice. Today, with two tools and one call each, helpers are arguably overkill. With three tools and 2-3 calls each, they're essential. Pre-emptive extraction before you have repeated calls is over-engineering; extraction after you've copied a block is good hygiene.
Cache + helper — combine?
Yes. Yesterday's caching pattern fits inside the helper:
_cache = {}
def add_task(title):
if "tasklist_id" not in _cache:
lists = toolset.execute_action(Action.GOOGLETASKS_LIST_TASK_LISTS, {})
items = lists.get("items") or lists.get("response_data", {}).get("items", [])
_cache["tasklist_id"] = items[0]["id"]
return toolset.execute_action(Action.GOOGLETASKS_CREATE_TASK, {
"tasklist_id": _cache["tasklist_id"],
"title": title,
})Now add_task is fast on call 2+. Every call site benefits without knowing about the cache.
def tool_a_action(args):
# discovery, defensive reads, the actual call
...
def tool_b_action(args):
...
# orchestration
result_a = tool_a_action(args)
intermediate = transform(result_a)
result_b = tool_b_action(intermediate)Each helper hides one tool's quirks. The orchestration is at the level of intent — which tool to call, in what order, with what data.
def add_task(title, notes=None):
"""Add a task to the user's default task list."""
if "tasklist_id" not in _cache:
lists = toolset.execute_action(Action.GOOGLETASKS_LIST_TASK_LISTS, {})
items = lists.get("items") or lists.get("response_data", {}).get("items", [])
if not items:
raise RuntimeError("no task lists")
_cache["tasklist_id"] = items[0]["id"]
args = {"tasklist_id": _cache["tasklist_id"], "title": title}
if notes:
args["notes"] = notes
result = toolset.execute_action(Action.GOOGLETASKS_CREATE_TASK, args)
return result.get("task", {}).get("id") or result.get("id") or result.get("response_data", {}).get("id")Five responsibilities, all hidden from callers:
title, helper builds the dictnotes only appears in the args if providedid, not the whole response dictReliability primitives compose into helpers naturally:
def add_task(title):
last_error = None
for attempt in range(1, 4):
try:
log("add_task.attempt", attempt=attempt, title=title)
id = ... # the actual call
log("add_task.ok", attempt=attempt, id=id)
return id
except Exception as e:
last_error = e
raise last_errorNow every caller of add_task automatically gets retry + structured logs. The orchestration loop stays simple.
def do_thing(x): return action(x) adds no value over inline action(x).Beyond ~5 helpers, move them to a separate file (automation_helpers.py) and from automation_helpers import add_task. That's Python Patterns territory; for v1 lessons, top-of-file is enough.
A script that talks to two tools — Tasks and Calendar — quickly accumulates boilerplate. Each tool has its discovery step, its response-shape quirks, its result.get(...) defensive reads.
The ugly version:
# add a task
lists = toolset.execute_action(Action.GOOGLETASKS_LIST_TASK_LISTS, {})
items = lists.get("items") or lists.get("response_data", {}).get("items", [])
toolset.execute_action(Action.GOOGLETASKS_CREATE_TASK, {
"tasklist_id": items[0]["id"],
"title": "task A",
})
# add a calendar event
toolset.execute_action(Action.GOOGLECALENDAR_CREATE_EVENT, {
"calendar_id": "primary",
"summary": "event A",
"start_datetime": "2026-12-31T10:00:00+00:00",
"event_duration_minutes": 30,
})The helper version:
def add_task(title):
lists = toolset.execute_action(Action.GOOGLETASKS_LIST_TASK_LISTS, {})
items = lists.get("items") or lists.get("response_data", {}).get("items", [])
return toolset.execute_action(Action.GOOGLETASKS_CREATE_TASK, {
"tasklist_id": items[0]["id"],
"title": title,
})
def add_event(summary):
return toolset.execute_action(Action.GOOGLECALENDAR_CREATE_EVENT, {
"calendar_id": "primary",
"summary": summary,
"start_datetime": "2026-12-31T10:00:00+00:00",
"event_duration_minutes": 30,
})
# orchestration reads cleanly
add_task("task A")
add_event("event A")The main flow names what it's doing. The plumbing is in the helpers.
When do I extract a helper?
When you'd write the same lines twice. Today, with two tools and one call each, helpers are arguably overkill. With three tools and 2-3 calls each, they're essential. Pre-emptive extraction before you have repeated calls is over-engineering; extraction after you've copied a block is good hygiene.
Cache + helper — combine?
Yes. Yesterday's caching pattern fits inside the helper:
_cache = {}
def add_task(title):
if "tasklist_id" not in _cache:
lists = toolset.execute_action(Action.GOOGLETASKS_LIST_TASK_LISTS, {})
items = lists.get("items") or lists.get("response_data", {}).get("items", [])
_cache["tasklist_id"] = items[0]["id"]
return toolset.execute_action(Action.GOOGLETASKS_CREATE_TASK, {
"tasklist_id": _cache["tasklist_id"],
"title": title,
})Now add_task is fast on call 2+. Every call site benefits without knowing about the cache.
def tool_a_action(args):
# discovery, defensive reads, the actual call
...
def tool_b_action(args):
...
# orchestration
result_a = tool_a_action(args)
intermediate = transform(result_a)
result_b = tool_b_action(intermediate)Each helper hides one tool's quirks. The orchestration is at the level of intent — which tool to call, in what order, with what data.
def add_task(title, notes=None):
"""Add a task to the user's default task list."""
if "tasklist_id" not in _cache:
lists = toolset.execute_action(Action.GOOGLETASKS_LIST_TASK_LISTS, {})
items = lists.get("items") or lists.get("response_data", {}).get("items", [])
if not items:
raise RuntimeError("no task lists")
_cache["tasklist_id"] = items[0]["id"]
args = {"tasklist_id": _cache["tasklist_id"], "title": title}
if notes:
args["notes"] = notes
result = toolset.execute_action(Action.GOOGLETASKS_CREATE_TASK, args)
return result.get("task", {}).get("id") or result.get("id") or result.get("response_data", {}).get("id")Five responsibilities, all hidden from callers:
title, helper builds the dictnotes only appears in the args if providedid, not the whole response dictReliability primitives compose into helpers naturally:
def add_task(title):
last_error = None
for attempt in range(1, 4):
try:
log("add_task.attempt", attempt=attempt, title=title)
id = ... # the actual call
log("add_task.ok", attempt=attempt, id=id)
return id
except Exception as e:
last_error = e
raise last_errorNow every caller of add_task automatically gets retry + structured logs. The orchestration loop stays simple.
def do_thing(x): return action(x) adds no value over inline action(x).Beyond ~5 helpers, move them to a separate file (automation_helpers.py) and from automation_helpers import add_task. That's Python Patterns territory; for v1 lessons, top-of-file is enough.
Create a free account to get started. Paid plans unlock all tracks.