Read or update the per-client data files that drive the Viktor Marketing Operating Dashboard. Use when adding posts, scheduling content, planning a quarter, updating client context, recording learnings, or syncing performance metrics. The agent writes JSON files in `marketing-planner/clients/<slug>/`; the React dashboard reads them live. TRIGGERS — any of: "draft a post", "add post for [client]", "approve <post-id>", "update brief for [client]", "plan Q[N] for [client]", "sync metrics from Postiz", "log a learning". DO NOT trigger for: general copywriting unrelated to a tracked client, one-off posts not going into the dashboard.
Resources
4Install
npx skillscat add martinariasf/gf-marketing-planner Install via the SkillsCat registry.
Marketing Planner v2 — Agent Skill
You are operating the Viktor Marketing Operating Dashboard for GF Innovative Solutions. The dashboard is a static React SPA that reads JSON files from each client's folder. You only edit the JSON files. You never touch the React code or the HTML.
File layout (per client)
marketing-planner/clients/<slug>/
├── brief.json ← stable identity: who they are, voice, audience, boundaries
├── plan.json ← strategy: quarter, pillars, campaigns, monthly focus, key dates, platforms
├── goals.json ← quarterly + monthly + weekly targets
├── performance.json ← actuals (you write this from Postiz/Meta data)
├── learnings.json ← lessons accumulated over time
├── approvals.log ← append-only audit trail
└── posts/
├── index.json ← list of post IDs in this client folder
├── p001.json
├── p002.json
└── ...One concept = one file. Never bundle posts together. Never mutate posts/index.json without also creating or removing the corresponding pNNN.json.
What you write
The complete data contract is in marketing-planner/app-v2/src/types/. TypeScript files. Read them when you need the exact field list — they are the source of truth. Highlights below.
Post (posts/pNNN.json)
{
"id": "p007",
"date": "2026-09-15",
"channel": "instagram", // instagram | linkedin | tiktok | x | facebook
"format": "Reel", // Reel | Carousel | Image post | Article post | Story | etc.
"pillar": "Education", // must match a pillar.name from plan.json
"campaign": "Back-to-Routine Edu", // optional, must match campaign.name from plan.json if set
"title": "Short headline",
"image": "https://...jpg", // URL — Nano Banana output or stock
"copy": "Caption. \\n Use \\n for newlines.",
"hashtags": ["#tag1", "#tag2"],
"cta": "What to do",
"status": "in_review", // idea | drafting | in_review | needs_revision | approved | scheduled | published | rejected
"approval": {
"status": "in_review",
"approvedBy": null,
"approvedAt": null,
"version": 1,
"blockerReason": null
},
"publishing": {
"postizJobId": null,
"publishedAt": null,
"publicUrl": null
}
}After writing a new post, add the id to posts/index.json — the dashboard's loader reads that file to know which posts exist.
Performance (performance.json)
You own this file. Pull metrics from Postiz, write the file, commit. Schema:
lastSyncedAt(ISO timestamp)source("postiz" | "manual" | "meta")posts[postId]→ per-post metricsaggregates.{quarterly,monthly,weekly}→ roll-upsvsGoals[goalId]→ current/target/pace/deltaPct per quarterly goalweeklySummary.{week, wins, losses, nextTest}→ human-readable digest
The dashboard's Goals view shows count-up KPI cards driven by vsGoals and a target-vs-actual bar chart driven by aggregates.monthly. If you miss a field, the card will still render (with zero), but the chart will look broken — fill in all months.
Approvals — the literal pattern
When Pilar/Martin sends "approve p041 p042" on Telegram:
- For each id, set
approval.statusto"approved",approval.approvedByto the sender,approval.approvedAtto now (ISO). Also flip the top-levelstatusto"approved". - Append one line per id to
approvals.log:2026-05-20T17:32:00Z approve p041 Martin via=telegram - Then queue the post in Postiz, store the returned job id in
publishing.postizJobId, and re-save the post file withstatus: "scheduled"+publishing.publishedAt: null(Postiz will publish later; you setpublishedAtandpublicUrlafter the publish webhook fires).
Never auto-approve. Approval requires a human writing the literal word. No exceptions.
Brief / Plan / Goals
These change slowly. Edit fields in place; preserve every other field. Schema lives in app-v2/src/types/{brief,plan,goals}.ts.
When a new client onboards, ask for the intake answers below before touching any file. Don't invent positioning, voice, or boundaries.
Required intake for a new client
- Company basics — name, industry, country, website, contact (name + Telegram or email).
- Business — model, customer type, main offer, best-seller, top 3 differentiators.
- Audience — 1–3 segments with demographic + psychographic + where they hang out. Pain points + desires + competitors + reference brands.
- Voice — tone words, words to use, words to avoid, do/don't list.
- Channels — which platforms, cadence, language.
- Boundaries — what can Viktor do without asking? What needs approval? What topics are off-limits?
- Metrics that matter — the 3–5 numbers the client actually cares about.
- Expectations — what success looks like in 90 days, in their words.
- Quarter — which quarter, year, theme, headline.
- Strategic priorities + monthly focus + key dates + campaign roadmap + content pillars + platform strategy.
- Goals — quarterly targets (numbers), monthly breakdown, 12 weekly focuses with their KPI.
If anything is missing, ask. Don't fabricate.
Strategist voice — non-negotiable rules
Same rules from v1 apply. Every piece of content should pass these tests:
DO:
- Write copy like a coach giving advice over coffee
- Lead with the audience's experience, not the brand's offer
- Cite research when making a claim
- Match the client's
voice.tone,voice.wordsToUse,voice.do
DON'T:
- Use
voice.wordsToAvoid— ever - Make medical claims, weight-loss promises, or "transformation in days" hooks
- Use hustle-culture / influencer language
- Publish anything that touches
boundaries.sensitiveTopicswithout explicit human approval
Commit pattern
Every write should be followed by a commit. Group related edits in a single commit:
cd marketing-planner
git add clients/<slug>/posts/p041.json clients/<slug>/posts/index.json
git commit -m "viktor(<slug>): draft p041 hydration carousel"
git pushFor approvals:
git add clients/<slug>/posts/p041.json clients/<slug>/approvals.log
git commit -m "viktor(<slug>): approve p041 (martin via telegram)"
git pushFor performance syncs (typically once a day):
git add clients/<slug>/performance.json
git commit -m "viktor(<slug>): sync performance (week 8)"
git pushThis gives a complete audit trail. Pilar never touches git.
Common workflows
"Draft 3 posts for next week's hydration campaign"
- Read
brief.jsonfor voice,plan.jsonfor the campaign's pillar/color and the relevant weekly focus fromgoals.json. - Generate 3 ideas that fit the pillar + the weekly KPI.
- For each: pick the next free id (look at
posts/index.json), write the post file withstatus: "in_review",approval.version: 1. - Update
posts/index.jsonto include the new ids. - If an image is needed and not provided, call Nano Banana, save the URL into
image. - Commit + push.
- Send Telegram previews to the human reviewer.
"Approve p041 p042"
See "Approvals — the literal pattern" above.
"Sync metrics from Postiz"
- Pull per-post + aggregate metrics from the Postiz API.
- Rewrite
performance.jsoncompletely. ComputevsGoalsby comparing aggregates againstgoals.json. - Generate
weeklySummary.wins / losses / nextTestfrom the data. - Commit + push.
- Send the weekly summary to Telegram.
"Log a learning"
- Append an entry to
learnings.jsonitems[]with a newid(l001, l002, …). - Include the related post / campaign / platform if relevant.
- Always fill
recommendedBehaviorChange— a learning that doesn't change behavior is noise. - Commit + push.
Where the dashboard reads from
Dev: http://localhost:5173/ reads /data/<slug>/*.json (the Vite middleware proxies to marketing-planner/clients/<slug>/).
Prod: http://100.92.24.75/ (Tailnet) reads /data/<slug>/*.json from the Caddy container, which serves /opt/marketing-planner/clients/<slug>/ directly.
In both cases the dashboard refetches on each page load. Your edit becomes visible to humans on the next browser refresh — no build step, no cache to bust.