Sales execution
Keep the next action close to the contact.
It started with…
Recruitment advisers move between contact records, follow-ups, appointments and messages. Each change of tool can mean finding the same context again.
So we built…
An Airtable workspace that brings contacts, tasks, calendar and communication tools together, with a campus or activity context and shared templates.
Take a look inside
Screen 1 of 6Working context
Select the relevant operational context.
What we set out to change
Make the next action easier to find and let the team reuse its templates and activity history.
How it is builtData model, code, controls and team ownership
The user journey
- 01
Choose a working context and find the contact or activity.
- 02
Open the relevant task, appointment or communication tool.
- 03
Keep shared configuration and activity history alongside the operational records.
Base structure
| Entity / table | Key fields | Relationships |
|---|---|---|
| Contacts | Identity · contact details · programme context | Contacts connect to activities and follow-ups |
| Actions | Date · type · recipient · status | Shared activity history |
| Templates | Communication type · content | Reusable message content |
| Configuration | Key · JSON value | Saved selections, profiles and mappings |
Technical implementation
- React modules and modals are loaded on demand; a debounced search limits work during typing.
- Phone normalisation is handled by a dedicated service.
- Configuration, record filtering and saved selections have separate hooks and helpers.
Engineering decision
Keep a large workspace responsive while it renders
Adapted from the CRM’s progressive rendering hook. A small first slice renders immediately; later slices are scheduled between browser frames, with cleanup when the view changes. This reduces rendering pressure, not Airtable data transfer: useRecords still loads the source set. The extracted version derives each slice from current records and scopes progress to the view ID.
import { useEffect, useState } from "react";import { useRecords } from "@airtable/blocks/interface/ui";export function useProgressiveRecords(view) { const records = useRecords(view); const [progress, setProgress] = useState({ viewId: view.id, count: 100 }); const count = progress.viewId === view.id ? progress.count : 100; useEffect(() => { let cancelled = false; let frame; let timer; let visible = Math.min(count, records.length); const advance = () => { timer = setTimeout(() => { frame = requestAnimationFrame(() => { if (cancelled) return; visible = Math.min(visible + 500, records.length); setProgress({ viewId: view.id, count: visible }); if (visible < records.length) advance(); }); }, 100); }; if (visible < records.length) advance(); return () => { cancelled = true; clearTimeout(timer); cancelAnimationFrame(frame); }; }, [view.id, records.length, count]); return { records: records.slice(0, count), complete: count >= records.length, total: records.length, };}Adapted implementation excerpt
Controls and boundaries
- Campus and activity filters are navigation context, not authorisation boundaries.
- External communications need scoped recipient selection, service credentials and a reviewed sending workflow. No messages were sent during this review.
Who can contribute
- Advisers: work in the contact, task and calendar interfaces.
- Builders: maintain templates, saved profiles and field mappings.
- Developers: evolve the modules and integrations with versioned source.
What supports this case
The native CRM guide, context selector and custom desktop were opened. The repository documents the modular interface and shared configuration model.