A skill is a Markdown file with YAML frontmatter, the shape an agent loads as a skill. myna keeps one per network and one or more per account, under the config dir, and reads them itself: the frontmatter carries limits the scheduler enforces, and the body is what a person or an agent reads before posting.
This exists because a blog went spammy. The same release was announced three
times, every bug fix became a post, and 29 pages went up in one day. The blog
skill says four a day and major features only, and myna holds the fifth post
to the next day rather than sending it. The cap paces automated promotion (the
launch announcement at the end of a ship, evergreen reposts, cross-account
copies). A post a person asks for by hand is sent with --now and goes out at
once, past the cap.
~/.config/myna/skills/
types/
launch-announcement/skill.md what a kind of post is, wherever it goes
bug-story/skill.md
...
htmlblog/
skill.md the network's rules
dev.profullstack.com-~anthony-blog/
skill.md this account's rules (the default)
launch-week.md another skill to pin or rotate to
bluesky/
skill.md
chovyfu.bsky.social/
skill.md
The account directory is the handle as one path segment: /, \, : and
spaces become -, and a leading @ is dropped. myna skill path <account>
prints the exact path, and myna skill list prints the segment.
- The built-in template, one per kind of network, in
packages/core/src/core/skills.ts. Never on disk; it is what the files are generated from. - The network skill,
skills/<network>/skill.md. One per network however many accounts are on it. Materialised from the template the first time the network is seen and yours to edit from then on. - The account skills,
skills/<network>/<account>/*.md.skill.mdis the generated default; anything else beside it was added withmyna skill add.
A post to an account resolves the account's selected skill, which inherits from the network skill, which inherits from the template. Limits merge with the stricter value winning.
Orthogonal to all of that is the post type, skills/types/<type>/skill.md:
what the post itself is (a launch announcement, release notes, a bug story),
whichever network it goes to. See below. The body an agent reads is the type
skill, then the account skill, then the network skill.
myna writes a file only when it is absent. A file you have edited is never
overwritten; myna skill init --force is the one way to regenerate from the
templates, and it says so.
---
name: myna-htmlblog
description: "How myna posts to HTML blog: what belongs there, how often, and the house style."
kind: blog
network: htmlblog
maxPerDay: 4
minGapMinutes: 60
requiresCanonical: false
contentPolicy: major-features-only
generatedFrom: blog@1
---| Key | Type | What myna does with it |
|---|---|---|
name, description |
string | The skill's identity, for an agent's skill loader. |
kind |
blog, social, forum, longform, directory, youtube, other |
Which template it came from. |
network, account |
string | Which network and account the file belongs to. Account files also carry profileUrl and connectedAt when known. |
maxPerDay |
number | Posts to this account in any rolling 24 hours. The scheduler holds a post past the cap to the moment the day's oldest post drops out of the window. |
minGapMinutes |
number | Least minutes between two posts to this account. Widens the global myna pace --gap when larger; never narrows it. Omitted where the global gap should rule. |
maxChars |
number | Characters in one post. Informational for the network's own limit; the social templates name it so an agent sees 300 for Bluesky, 500 for Mastodon, 3000 for LinkedIn, 280 for X. |
requiresCanonical |
boolean | True on the longform mirrors (dev.to, Hashnode, Ghost, WordPress, Tumblr): an agent must pass --canonical-url pointing at our own page. |
contentPolicy |
string | major-features-only on a blog. Free text elsewhere. The most specific layer that names one wins. |
generatedFrom |
string | The template and version the file was written from. |
Anything else you add to the frontmatter is kept as written.
For each key, the layers that name a value are compared and the stricter one
wins: the lower maxPerDay, the larger minGapMinutes, the lower maxChars,
requiresCanonical true if any layer says so. A value in any file beats the
template, so a blog file that says maxPerDay: 6 means 6; between files the
tighter one holds, so a network skill at 2 caps an account skill at 6 to 2.
blog.maxPerDay in settings.json stands in for the blog template's default
when no file names a value. A value in a skill file always wins over it.
A rotated skill is merged on top of the account's default skill the same way, so it can tighten the cap and never loosen it.
blog (gitblog, htmlblog, bl0ggers): maxPerDay: 4, contentPolicy: major-features-only. Major feature announcements and launches only; no
bug-fix stories, no small updates, no re-sends; one post may bundle several
related features; the blog is the canonical original and every mirror points
at it; house style (no em dashes, no LLM tells, concrete details, a real
title); what belongs on the blog versus the socials.
social (Bluesky, Mastodon, LinkedIn, X, Facebook, Threads, the chat
networks): the network's character cap, URL last, one post per launch, reposts
spaced by the pacing settings, no hashtag spam. maxPerDay: 12, which is what
a two-hour gap already allows, so nothing already queued moves.
forum (tsbb, Reddit, Lemmy): forum cycling, a real headline in the first
sentence, reply etiquette, one topic per release. maxPerDay: 8.
longform (dev.to, Hashnode, Ghost, WordPress, Micro.blog, Tumblr): a mirror
of a blog post, never an original; always --canonical-url. maxPerDay: 4,
requiresCanonical: true.
directory (SaaSRow and any MCP directory): one listing per product,
updated rather than resubmitted. maxPerDay: 1.
youtube: a post is a comment on a video found with myna search; short,
on topic, never the same comment on several videos. maxChars: 500.
A type says what a post is, wherever it goes. Nine ship built in, materialised
by myna skill init and yours to edit; add your own with myna skill add --type <slug> --from file.md. myna post --type <slug> names one; without
it, a post that reaches a blog or a longform mirror is a launch-announcement
and anything else is a social-update.
| Type | Allowed on | Cap | Structure |
|---|---|---|---|
launch-announcement |
blog, longform, social, forum | what-it-is, who-it-is-for, what-changed, how-to-use-it, link | |
release-notes |
social, forum, longform | version, what-changed, upgrade-note, link | |
bug-story |
social, forum | what-broke, why, the-fix, the-lesson | |
essay |
blog, longform, social, forum | 1/day | the-claim, the-evidence, the-counter, so-what |
repost |
longform | canonical-url, same-body | |
promo |
social, forum | 2/day | the-offer, for-whom, the-deadline, link |
reply |
forum, social, youtube | the-answer, the-detail, optional-link | |
event |
social, forum, other | what, when, where, link | |
social-update |
social, forum, youtube, other | what-happened, link |
Frontmatter: name, description, type, allowedKinds (a flow list of
network kinds), maxPerDay (per type across every account, optional),
requiresUrl, structure (the sections, in order, an agent writes),
generatedFrom.
What myna enforces:
allowedKinds. A type is refused on a target whose kind it does not list, with a clear error and no queue entry. Abug-storytohtmlblogis refused; that is the blog policy written down once. The daemon also drops a queued entry whose type its target no longer carries.maxPerDayon the type, across every account, on top of the network and account caps. Both apply, so the stricter one wins: one essay a day, two promos a day.
The type is written onto the queue entry (type) and the history entry, and
myna queue, myna history and the dashboard show it.
myna skill show type:bug-story # or: myna skill show bug-story --type
myna skill path type:essay
myna skill add --type changelog --from changelog.md
myna skill remove --type changelog # a built-in comes back on init
myna post --to all --type release-notes "myna 0.19.0: ..."myna skill add bluesky:chovyfu.bsky.social launch-week --from launch-week.md
echo "Say less." | myna skill add bluesky:chovyfu.bsky.social quiet
myna skill default bluesky:chovyfu.bsky.social quiet # pin one
myna skill rotate bluesky:chovyfu.bsky.social on # or take turns
myna skill remove bluesky:chovyfu.bsky.social quietWith rotation on, each send takes the next skill in order after the one used
last (the default first, then the rest alphabetically). The cursor lives in
settings.json under skills.cursor, not in the files, so editing a skill
never moves it and moving it never rewrites a skill. Pins live under
skills.defaults and rotation flags under skills.rotate. With rotation off
the pinned skill is used, and without a pin the generated skill.md.
Reading a skill (the CLI, the dashboard, MCP) never moves the cursor; only a
send does. Each history entry records the skill it used, and myna history
and the dashboard show it.
maxPerDayat queue time and again at send time. The count is the account's successful sends in the last 24 hours plus its pending queue entries earlier in that window. A post over the cap goes to the next free slot after the day rolls over, then clears the network gap from there.--frontopens the network gates, not the day's budget.--nowopens both: it means a person asked for this one by hand, and the budget exists to pace automated promotion, which never passes--now.minGapMinuteswidens the network gap for that account when larger.- Duplicate titles. A post to a blog or a longform mirror whose title (the
--title, else the first line) is already in that account's history is refused with an error, not queued.--allow-duplicateoverrides it, and rides on the queue entry so the daemon honours it. The daemon also drops a queued blog post whose title went out after it was queued.
CLI
myna skill list # every network and account, which skill is on
myna skill show htmlblog # the network skill
myna skill show htmlblog:dev.profullstack.com/~anthony/blog
myna skill init [--force] # write the missing files
myna skill path <network[:account]> [slug]myna login writes the network and account skill when it connects an
account. myna skill init does the same for every account already connected.
Dashboard (myna dashboard, 127.0.0.1:7777)
GET /skills index, Markdown (/skills.json for data)
GET /types/skill.md the post types, as an index
GET /types/:type/skill.md one post type
GET /:network/skill.md the network skill
GET /:network/:account/skill.md the account's selected skill
GET /:network/:account/skills/ its skills, as a list
GET /:network/:account/skills/:slug.md one of them
:account is the handle as a path segment. Everything is served as
text/markdown. The page itself has a Skills section with each account's
skill, how many of today's posts are used, and its limits.
MCP: myna_skills lists the types, networks and accounts; myna_skill
returns one account's skill, its network's skill, the merged limits and, with
type, the type skill first plus whether that kind carries it. myna_post
takes type and allow_duplicate.
Before posting, pick the type and read its skill, then the account's:
myna skill show type:<slug> and myna skill show <account>,
GET /types/<type>/skill.md and GET /<network>/<account>/skill.md on the
dashboard, or the myna_skill tool with type and account.
Follow the body and stay inside the frontmatter limits. myna enforces the
limits either way, but a post that was written to the rules does not end up
queued for tomorrow or refused as a repeat.