Restaurant menu optimisation from sales data and cost analysis
- Published
Running a restaurant means juggling competing priorities: keeping customers happy, managing food costs, and maximising profits. Your menu is the intersection of all three. But most restaurants optimise menus manually, if at all. A manager glances at which dishes sell well, notices which ones have high food costs, and maybe updates the menu once a year.
The problem is that your data changes constantly. Your supplier raises the price of salmon; your pasta sales spike on Tuesdays; waste from a particular dish varies by season. Manual menu optimisation means you're always behind the curve, leaving money on the table.
What if your menu could respond to real data automatically? Every week, your sales figures could feed into a cost analysis, which then generates fresh menu descriptions designed to highlight high-margin items, adjust pricing strategies, and retire underperformers. This workflow can run hands-off, without anyone manually copying spreadsheets or rewriting descriptions.......... For more on this, see Competitive SaaS pricing analysis and dynamic rate card g....
The Automated Workflow
Overview of the process
The workflow chains together three tools: accio-ai gathers and structures your sales and cost data; terrakotta-ai analyses margins and profitability; copy-ai writes compelling menu descriptions that steer customers towards your best items. An orchestration layer (Zapier, n8n, or Make) triggers the whole chain on a schedule or when new data arrives.
Here's the data flow:
-
Sales data enters the system (from your POS, spreadsheet, or database).
-
accio-ai structures the data and pulls in cost information.
-
terrakotta-ai runs margin analysis and flags which dishes to promote.
-
copy-ai generates new menu descriptions based on the analysis.
-
Results are exported to your menu management system or spreadsheet.
No human intervention between steps one and five.
Choosing your orchestration tool
For this workflow, I'd recommend n8n or Make over Zapier for most restaurants. Here's why: the workflow involves conditional branching (only promoting dishes above a certain margin), multiple API calls in sequence, and data transformation between steps. Zapier excels at simple two-step automations; n8n and Make handle complexity better and cost less at scale.
n8n is self-hosted (you control the server) and has no rate limits on internal nodes, which matters when you're moving data between three different tools. Make has a cleaner visual editor if you're new to workflow automation.
We'll build this example in n8n, but the logic translates directly to Make or Zapier.
Step 1: Trigger and data ingestion
Your workflow starts when new sales data arrives. This could be a daily export from your POS system, a webhook from a spreadsheet, or a scheduled check at 6 AM every Monday.
Trigger Type: Webhook (if data is pushed to n8n)
or
Schedule (if n8n pulls data on a recurring basis)
Example webhook structure (from your POS):
{
"date": "2024-01-15",
"sales": [
{
"dish_id": "pasta_001",
"dish_name": "Carbonara",
"quantity_sold": 24,
"revenue": 288.00,
"food_cost": 68.00
},
{
"dish_id": "salmon_002",
"dish_name": "Pan-fried Salmon",
"quantity_sold": 18,
"revenue": 342.00,
"food_cost": 144.00
}
]
}
In n8n, add an incoming webhook node. Configure it to receive POST requests. The webhook URL looks like this:
https://your-n8n-instance.com/webhook/menu-optimisation
Test by sending sample data using curl or Postman:
curl -X POST https://your-n8n-instance.com/webhook/menu-optimisation \
-H "Content-Type: application/json" \
-d '{
"date": "2024-01-15",
"sales": [
{
"dish_id": "pasta_001",
"dish_name": "Carbonara",
"quantity_sold": 24,
"revenue": 288.00,
"food_cost": 68.00
}
]
}'
Step 2: Data enrichment with accio-ai
accio-ai's job is to pull in additional context: supplier costs, historical data, ingredient breakdowns. If your sales webhook only contains revenue and food cost, accio-ai can fetch your full cost ledger and match it to each dish.
Add an HTTP request node pointing to accio-ai's API:
Endpoint: https://api.accio-ai.com/v1/analyse/cost-structure
Method: POST
Authentication: Bearer YOUR_ACCIO_API_KEY
The request body:
{
"dishes": [
{
"dish_id": "pasta_001",
"name": "Carbonara",
"ingredients": ["eggs", "guanciale", "pecorino", "pasta"],
"portion_size": "400g"
},
{
"dish_id": "salmon_002",
"name": "Pan-fried Salmon",
"ingredients": ["salmon_fillet", "lemon", "herbs", "olive_oil"],
"portion_size": "200g"
}
],
"cost_period": "current_month"
}
accio-ai returns enriched data with current ingredient costs and historical trends:
{
"enriched_data": [
{
"dish_id": "pasta_001",
"dish_name": "Carbonara",
"ingredient_cost": 2.83,
"labour_cost": 1.20,
"total_cost_per_portion": 4.03,
"cost_trend": "stable"
},
{
"dish_id": "salmon_002",
"dish_name": "Pan-fried Salmon",
"ingredient_cost": 8.00,
"labour_cost": 1.50,
"total_cost_per_portion": 9.50,
"cost_trend": "increasing"
}
]
}
In n8n, map the incoming sales data to accio-ai's expected format using a Function node:
return items.map(item => {
const sales = item.json.sales;
const dishes = sales.map(dish => ({
dish_id: dish.dish_id,
name: dish.dish_name,
ingredients: getIngredientsFromDatabase(dish.dish_id),
portion_size: getPortion(dish.dish_id)
}));
return {
json: {
dishes: dishes,
cost_period: "current_month"
}
};
});
Parse the response and create a combined dataset: sales + costs + trends. You now have the full financial picture for each menu item.
Step 3: Margin analysis with terrakotta-ai
Now run the enriched data through terrakotta-ai to identify which dishes are truly profitable, accounting for popularity and waste rates.
Endpoint: https://api.terrakotta-ai.com/v1/margin-analysis
Method: POST
Authentication: Bearer YOUR_TERRAKOTTA_API_KEY
Request body:
{
"dishes": [
{
"dish_id": "pasta_001",
"name": "Carbonara",
"selling_price": 12.00,
"food_cost": 4.03,
"units_sold_last_week": 24,
"waste_percentage": 3.5
},
{
"dish_id": "salmon_002",
"name": "Pan-fried Salmon",
"selling_price": 19.00,
"food_cost": 9.50,
"units_sold_last_week": 18,
"waste_percentage": 8.2
}
],
"analysis_type": "gross_profit",
"benchmark_margin": 65
}
terrakotta-ai returns a scored list, flagging high-performers and underperformers:
{
"analysis_results": [
{
"dish_id": "pasta_001",
"name": "Carbonara",
"selling_price": 12.00,
"food_cost_adjusted": 4.17,
"gross_margin_percentage": 65.3,
"margin_rank": "high",
"weekly_profit_contribution": 191.88,
"recommendation": "promote",
"reason": "High margin, high volume, low waste"
},
{
"dish_id": "salmon_002",
"name": "Pan-fried Salmon",
"selling_price": 19.00,
"food_cost_adjusted": 10.29,
"gross_margin_percentage": 45.8,
"margin_rank": "low",
"weekly_profit_contribution": 155.52,
"recommendation": "adjust_price_or_cost",
"reason": "Margin below benchmark; high waste"
}
]
}
In n8n, use a Filter node to separate dishes into promotion candidates and candidates for price adjustment:
return items.map(item => {
const results = item.json.analysis_results;
const toPromote = results.filter(d => d.recommendation === 'promote');
const toAdjust = results.filter(d => d.recommendation === 'adjust_price_or_cost');
return {
json: {
promote: toPromote,
adjust: toAdjust,
all_results: results
}
};
});
This branching is critical: dishes recommended for promotion get compelling, benefit-focused copy; dishes needing adjustment get different treatment (emphasise value, highlight specials).
Step 4: Copy generation with copy-ai
Now copy-ai writes fresh menu descriptions. You'll make two separate API calls, one for promoted dishes and one for adjusted dishes, so the tone and messaging differ.
For promoted items:
Endpoint: https://api.copy-ai.com/v1/generate/menu-copy
Method: POST
Authentication: Bearer YOUR_COPYAI_API_KEY
Request body for promoted dishes:
{
"content_type": "menu_description",
"tone": "enticing_premium",
"input": {
"dishes": [
{
"name": "Carbonara",
"margin_status": "high",
"reason": "High margin, high volume, low waste",
"current_price": 12.00,
"key_ingredients": ["eggs", "guanciale", "pecorino", "pasta"],
"sell_points": ["authentic", "popular", "creamy"]
}
],
"instruction": "Write a 1-2 sentence menu description that highlights why this dish is special. Emphasise quality and tradition."
}
}
copy-ai returns polished descriptions:
{
"generated_copy": [
{
"dish_id": "pasta_001",
"name": "Carbonara",
"new_description": "Our signature carbonara, made with guanciale and fresh eggs, has been a guest favourite for years. Creamy, authentic, and consistently perfect.",
"description_style": "premium"
}
]
}
For dishes needing price or cost adjustment, use a different prompt:
{
"content_type": "menu_description",
"tone": "value_focused",
"input": {
"dishes": [
{
"name": "Pan-fried Salmon",
"margin_status": "below_target",
"reason": "High waste; margin below benchmark",
"current_price": 19.00,
"key_ingredients": ["salmon_fillet", "lemon", "herbs"],
"sell_points": ["fresh", "sustainable", "seasonal"]
}
],
"instruction": "Write a 1-2 sentence menu description that emphasises freshness and value. Make the dish sound worth the price."
}
}
In n8n, chain two HTTP request nodes (one for each group) in parallel, then merge the results:
// After both copy generation calls complete, merge outputs
return items.map(item => {
const promoted = item.json.promoted_copy || [];
const adjusted = item.json.adjusted_copy || [];
return {
json: {
all_new_descriptions: [...promoted, ...adjusted]
}
};
});
Step 5: Export and publishing
Finally, save the results to your menu management system, a spreadsheet, or a database. Most restaurants use either a Google Sheet or a dedicated menu system.
Export to Google Sheets using n8n's built-in Google Sheets node:
Node Type: Google Sheets
Action: Append
Spreadsheet: Menu Optimisation (your sheet)
Sheet Name: Latest Updates
Columns:
- Dish ID
- Dish Name
- New Description
- Recommendation (Promote / Adjust Price / etc.)
- Gross Margin %
- Weekly Profit Contribution
Alternatively, use a webhook to POST results to a custom menu API:
POST https://your-menu-system.com/api/menu-updates
Content-Type: application/json
Authorization: Bearer YOUR_API_KEY
{
"update_date": "2024-01-15",
"menu_changes": [
{
"dish_id": "pasta_001",
"new_description": "Our signature carbonara...",
"recommendation": "promote",
"margin_percentage": 65.3
}
]
}
Complete n8n workflow configuration
Here's how the workflow nodes connect in n8n:
1. Webhook Trigger
↓
2. Function: Map sales to accio-ai format
↓
3. HTTP Request: Call accio-ai
↓
4. Function: Merge sales + cost data
↓
5. HTTP Request: Call terrakotta-ai
↓
6. Function: Split into promote / adjust branches
↓
├→ 7a. HTTP Request: copy-ai for promoted items
│ ↓
│ 8a. Merge promoted copy
│
└→ 7b. HTTP Request: copy-ai for adjusted items
↓
8b. Merge adjusted copy
↓
9. Function: Combine all results
↓
10. Google Sheets / Webhook: Export
The Manual Alternative
If you prefer more control or want to validate the automation's recommendations before they go live, run the workflow in review mode.
After step 5 (export), instead of automatically publishing to your menu system, send results to a Slack channel or email digest for your manager to review. A human can then confirm that the recommendations make sense before updating the live menu.
This adds a day or two of latency but eliminates the risk of automated decisions going wrong due to bad data (e.g., if a cost figure is entered incorrectly in accio-ai).
To implement review mode in n8n, add a Slack notification node before the export:
Node Type: Slack
Action: Send Message
Channel: #menu-team
Message Template:
"Menu optimisation complete. Review changes: [link to Google Sheet]
Approve by replying with ✓ or ✗"
Then conditionally proceed to export only if approval is given. This requires a Slack Workflow integration, which adds complexity but keeps humans in the loop.
Pro Tips
Rate limiting and API throttling
accio-ai, terrakotta-ai, and copy-ai all have rate limits. If you're running this workflow weekly across 50+ dishes, you might hit limits. Add deliberate delays between API calls in n8n using the Wait node:
After accio-ai call: Wait 2 seconds
After terrakotta-ai call: Wait 3 seconds
Between copy-ai batches: Wait 5 seconds
Alternatively, request higher API quotas from each provider if you're running daily workflows. Most offer tiered plans.
Error handling and retries
Network failures happen. Configure retry logic in n8n for all HTTP nodes. Set up 3 retries with exponential backoff:
Node: HTTP Request (all three API calls)
On Error: Retry
Retry Count: 3
Retry Interval: 1000ms (first), 2000ms (second), 4000ms (third)
If all retries fail, send a Slack alert to your ops team so they know the workflow didn't complete.
Cost optimisation
Each API call costs money. Reduce costs by:
-
Running the workflow weekly instead of daily (unless your costs change rapidly).
-
Only analysing the top 30 dishes by volume, not every single item on your menu.
-
Caching accio-ai's cost data for 7 days, so you're not re-fetching unchanged supplier costs every run.
To cache in n8n, use a local data store:
// On first run, fetch full cost data
if (!workflowData.cachedCosts) {
workflowData.cachedCosts = accioResponse.data;
workflowData.cacheDate = new Date();
}
// On subsequent runs within 7 days, use cached data
const daysSinceCached =
(new Date() - workflowData.cacheDate) / (1000 * 60 * 60 * 24);
if (daysSinceCached < 7) {
return { json: workflowData.cachedCosts };
}
Data validation
Before sending data to terrakotta-ai or copy-ai, validate it. A single malformed record can break the analysis. Add a Function node after accio-ai that checks:
return items.map(item => {
const dishes = item.json.enriched_data;
const valid = dishes.filter(d => {
const hasRequired = d.dish_id && d.name && d.ingredient_cost !== null;
const costsValid = d.ingredient_cost > 0 && d.labour_cost >= 0;
return hasRequired && costsValid;
});
const invalid = dishes.filter(d => !valid.includes(d));
if (invalid.length > 0) {
// Log invalid records and alert
console.log("Invalid records:", invalid);
}
return { json: { valid_dishes: valid } };
});
Scheduling and frequency
Most restaurants' costs and sales patterns shift weekly, so run this workflow every Monday at 6 AM. In n8n, replace the Webhook trigger with a Schedule trigger:
Trigger Type: Schedule
Repeat: Weekly
Day of Week: Monday
Time: 06:00 (UTC)
If you operate across multiple locations or have highly variable costs, run daily instead. Just be aware of the API costs.
Cost Breakdown
| Tool | Plan Needed | Monthly Cost | Notes |
|---|---|---|---|
| accio-ai | Pro | £50–100 | Depends on API calls; ~100 calls/week at standard tier |
| terrakotta-ai | Starter | £40–80 | Analysis-only tool; low call volume |
| copy-ai | Growth | £60–120 | Text generation can be pricey; batching helps |
| n8n | Cloud (paid) or self-hosted | £0–50 | Free self-hosted; paid cloud scales with executions |
| Make (alternative) | Standard | £10–20 | Cheaper than n8n for simple workflows |
| Zapier (alternative) | Professional | £50–100 | Pricier but user-friendly UI |
| Total | – | £160–350 | Varies by volume and execution frequency |
The exact cost depends on how many dishes you analyse, how often you run the workflow, and API overage fees. A small restaurant (20 dishes, weekly runs) sits at the lower end; a chain with multiple locations (100+ dishes, daily runs) approaches the higher end.
Most tools offer free tiers or trials, so test the workflow with a few dishes before committing to paid plans.
More Recipes
User onboarding video series from feature documentation
SaaS companies need to convert technical documentation into engaging onboarding videos for different user segments.
Course curriculum and assessment generation from subject outline
Educators spend weeks designing course materials and assessments when they could generate them from a high-level curriculum outline.
Technical documentation generation from code
Developers struggle to maintain up-to-date documentation alongside code changes.