Authoring Procedures
How to turn your company's informal processes into structured procedure documents that Gateway can execute.
Interview Flow
When creating a procedure, work through these questions:
- What is this? Name it. One sentence on what it does.
- Who triggers it? Manual (someone asks), scheduled (recurring), or event-driven?
- What info is needed to start? These become Input fields.
- What are the steps? Walk through the happy path first, then edge cases.
- Where does it wait? Which steps need human input, approval, or a deadline?
- What gets tracked? Collections (Notion DBs) vs. scalar state.
- What's the output? What does "done" look like?
- Who needs to know? Slack notifications, approvals, announcements.
- What does it touch? Which Slack channels, Linear teams, Notion databases?
Document Structure
Every procedure follows this structure:
[frontmatter]
# Title
[1-2 sentence description]
## Input
[table of fields needed to start]
## State
[table of what's tracked during execution]
## Steps
[named steps with descriptions and transitions]
## Output
[table of what the completed procedure produces]Frontmatter
id: expense-report
version: 1.0
trigger: manual
category: Finance
description: Submit expenses for reimbursement with multi-tier approval routing.
access: public
admins:
- slack:#finance-team
share:
- slack:#all-employees| Field | Required | Values |
|---|---|---|
id | yes | Unique, kebab-case |
version | yes | major.minor — LLM bumps based on nature of edits |
trigger | yes | manual, schedule("cron"), event("type") |
category | yes | Finance, People, Procurement, Access, Compliance, Operations, Custom |
description | yes | 1-2 sentence user-facing summary (what this does, when to use it) |
access | yes | public (discoverable by all) or private (only shared users) |
admins | yes | List of admin principals: slack:#channel, email:user@co.com, or user:{id} |
share | no | List of principals who can discover and invoke (if private) |
Principal Format
Access control uses principals that can reference Slack channels (as groups), emails, or user IDs:
admins:
- slack:#it-internal # everyone in this Slack channel is an admin
- email:cfo@company.com # specific user by email
share:
- slack:#all-employees # everyone in the company
- email:contractor@ext.com # specific external userSlack channel membership is dynamically resolved — when someone joins or leaves the channel, their access updates automatically.
Input Table
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| amount | number | yes | | Total in USD |
| category | string | yes | | meals / travel / software |Types: string, number, date, boolean, string[], object
Mark fields required only if the procedure can't start without them. Always provide sensible defaults where possible.
State Table
| Field | Storage | Description |
|-------|---------|-------------|
| expense_record | Notion DB row | The expense entry |
| current_step | invocation | Where we are |Notion DB— for collections (submissions, approvals, checklist items)Notion DB: {auto}— AI creates the schema from step contextNotion DB row— single row in an existing databaseinvocation— scalar value stored on the run record
Always include current_step | invocation | Where we are.
Steps
Each step is an ### h3 heading under ## Steps.
### route_approval
Determine the approver based on amount.
Post in #finance-approvals for approval.
**Wait**: 48h
**On event** `approved`: → process_reimbursement
**On event** `rejected`: → notify_rejection
**On timeout**: Escalate — ping approver again, CC finance lead.
→ process_reimbursementConventions
| Syntax | Meaning |
|---|---|
→ step_name | Transition to next step |
→ done ✓ | Final step (every procedure needs at least one) |
→ step_a | step_b | Branch (AI chooses based on conditions) |
**Wait until**: {condition} | Pause until date/time |
**Wait**: {duration} | Pause for a duration (3d, 48h) |
**On event** \name`: [action]` | Handle event during wait |
**On timeout**: [action] | Handle wait expiry |
{input.field} / {state.field} | References to input and state |
Step Naming
Use snake_case, descriptive names.
| ✅ Good | ❌ Bad |
|---|---|
route_approval | step3 |
notify_rejection | next |
collect_submissions | do_stuff |
Output Table
| Field | Type | Description |
|-------|------|-------------|
| expense_id | string | Notion page ID |
| status | string | approved / rejected |What the completed procedure produces — IDs of created records, summary stats, status strings.
Quality Checklist
Before finalizing a procedure, verify:
- Every step has a
→transition (no dead ends) - At least one
→ done ✓final step - All
{input.field}refs exist in the Input table - All
{state.field}refs exist in the State table - Wait steps have both
On eventandOn timeout - Slack notifications say WHO needs to act and HOW
- ID is unique and kebab-case
-
descriptionis 1-2 sentences, user-facing -
accessis set (publicorprivate) -
adminslists at least one principal -
versionusesmajor.minorformat
Anti-Patterns
| Don't | Do |
|---|---|
| Steps that just say "do the thing" | Describe WHAT, WHERE, and WHAT to say |
| Hardcoded names/IDs | Use {input.field} references |
| Wait without timeout handling | Always have **On timeout** |
| Approval with no escalation | Add escalation after reasonable wait |
| Skipping Slack notifications | People need to know what's happening |
| Giant monolith steps | Split into focused steps with clear transitions |
step1, step2 naming | Descriptive: route_approval, notify_team |