Documentation and tooling for the AnyCross workflow file format (flow.json), packaged as an AI agent skill that generates, validates, and packages AnyCross workflows into importable .zip files.
AnyCross is Lark/Feishu's automation platform (their answer to Zapier or Make). It has no public schema documentation for the exported workflow file — so everything here was reverse-engineered from real, running exports: connector IDs, operation IDs, the node shape, the four spel reference types, LarkBase field read/write formats, and the error messages you hit along the way.
The packaging follows the Agent Skills convention (a SKILL.md plus supporting files), which Claude Code loads automatically. But nothing inside is Claude-specific:
| Layer | What it is | Portable to |
|---|---|---|
SKILL.md |
The build procedure, in plain markdown | Any agent that reads instructions — Cursor, Copilot, Codex, Gemini CLI, Windsurf, Cline. Paste it as context or a system prompt. |
references/ |
The AnyCross schema documentation | Anything. It's just markdown. Feed it to your model, or read it yourself. |
scripts/ |
inspect_export.py, validate_flow.py, pack_flow.py |
Plain Python 3.8+, standard library only. No AI involved — run them from a terminal or a CI job. |
So use it three ways: let an agent drive the whole loop, hand your model the reference files as context, or ignore the AI part entirely and use the scripts plus docs by hand.
This is a documentation-first project that happens to ship as a skill — the schema knowledge is the asset, and it outlives whichever agent you point at it.
Hi everyone,
This is a skill for creating AnyCross workflows — I hope you find it useful. If you need any help, contact me at phucvn2019@gmail.com
Thank you,
Phuc
Writing AnyCross flow.json by hand is unforgiving. The logic is the easy part — the trap is that every connector ID, operation ID, credential ID, and parameter shape is tenant- and version-specific.
Two Bitable connectors exist in the wild:
| Version | connectorId | "update a record" operationId |
|---|---|---|
| v2.0 | 7241926900765982725 |
7241926901814525957 |
| v2.7 | 7576576101812538807 |
7576576102517181878 |
Different IDs, different operation IDs, different parameter names for the same action. An LLM guessing these produces a zip that either fails to import or — worse — imports cleanly and silently does the wrong thing.
So this skill is built around one rule: copy IDs from a real export, never from memory. Everything in it was reverse-engineered from real, running AnyCross exports rather than from documentation.
git clone https://github.com/helleOPP/anycross-skill-creator.gitRequirements: Python 3.8+. No third-party packages — standard library only.
Claude Code — drop the folder into a skills directory and it loads on its own:
cp -r anycross-skill-creator/anycross-creator ~/.claude/skills/ # every project
cp -r anycross-skill-creator/anycross-creator /path/to/project/.claude/skills/ # one projectCheck it registered with /anycross-creator.
Cursor / Copilot / Codex / Gemini CLI / any other agent — there's no plugin to install. Point your model at the files:
- Paste
anycross-creator/SKILL.mdin as context, a system prompt, or a rules file (.cursorrulesand friends). - When it starts building, give it the relevant
references/*.mdtoo —connectors.mdandflow-schema.mdcarry the IDs and shapes it would otherwise hallucinate.
No agent at all — the scripts stand alone:
python anycross-creator/scripts/inspect_export.py "My Flow.zip"Just describe the automation you want. The skill triggers on its own when you mention AnyCross, a flow.json, or a Lark automation:
Build me an AnyCross workflow: every day at 6am, read the pending rows from my Bitable, and post a summary card to a Lark group.
Claude then walks a five-step loop:
| Step | What happens | Why it matters |
|---|---|---|
| 0. Ask for a template | Claude asks for a .zip export of any workflow already running in your tenant |
The single highest-leverage step — see below |
| 1. Constants | Collects app_token, table_id, credentials — or wires them to AnyCross project variables so no tenant IDs land in the file at all |
Keeps the flow portable and free of secrets |
| 2. Generate | Writes a Python generator script that emits flow.json |
Reproducible and reviewable, unlike hand-edited JSON |
| 3. Validate | Runs the pre-import checks (below) | Catches in seconds what costs minutes to find in the UI |
| 4. Test & learn | You import it and report any error verbatim; Claude diagnoses, fixes, and writes the lesson into the skill | The skill gets smarter with every failure |
| 5. Report | Tells you what was built, what it depends on, and which parts are unverified | No silent guesses |
If you have any AnyCross workflow already running in the same tenant, export it and hand over the path. It gives Claude, for free:
- the exact connector versions your tenant has installed
- working credential IDs
- the real parameter names for each operation
- spel reference paths that actually resolve
Without one, Claude falls back to the bundled inventory and will tell you plainly that the first import is a probe, not a delivery.
Claude cannot see the AnyCross UI — you are its only sensor. When something fails, it asks for specific things (the red banner text verbatim, the failed node's Input panel, the previous node's Output panel), because those show the real shape of the data instead of a guess.
Once diagnosed, the lesson is appended to references/known-errors.md as a class of mistake rather than an incident, so the next build avoids it instead of re-discovering it.
All three are standalone — usable without Claude.
Inspect a real export — the anti-hallucination tool. Prints every node's connector name/version, connectorId, operationId, parameter keys, credentials, and optionally every spel reference:
python anycross-creator/scripts/inspect_export.py "My Flow.zip"
python anycross-creator/scripts/inspect_export.py "My Flow.zip" --spel
python anycross-creator/scripts/inspect_export.py "My Flow.zip" --node bitable-1Validate before importing — checks UTF-8 validity, structure ↔ steps parity (including nested subSteps), node shape (operation present, js_code not code, credentials under auth not parameters), and spel drift per node_tree type:
python anycross-creator/scripts/validate_flow.py out/flow.jsonIt is calibrated against real production exports, so an error means a real defect, not a style opinion. A clean run means the zip will import — it does not mean the flow is correct; wrong operation IDs and wrong field names pass every static check and fail at runtime.
Package — validates, then wraps into an importable zip (and refuses to pack a failing flow):
python anycross-creator/scripts/pack_flow.py out/flow.json "out/My Workflow.zip"anycross-creator/
├── SKILL.md # the workflow Claude follows
├── references/
│ ├── flow-schema.md # flow.json anatomy: structure/steps, node shape,
│ │ # value types, the four spel types, loop/branch/while
│ ├── connectors.md # verified connector/operation inventory
│ ├── larkbase.md # Bitable field read/write formats, filters, extract() helpers
│ ├── integrations.md # HTTP, Lark card, Google Docs, e-signature, subflow, cron
│ └── known-errors.md # the error ledger — read before debugging, append after
├── scripts/
│ ├── inspect_export.py # harvest IDs from a real export
│ ├── validate_flow.py # pre-import checks
│ └── pack_flow.py # validate + zip
└── assets/
└── generator_skeleton.py # starting point, with all the value-type helpers
- Node IDs live inside an
operationobject —{connectorId, operationId, connectorName, connectorVersion}— not at the top level of the node. - Script node output is namespaced under
.result. A handler returning{rows: [...]}inscript-2is read as$.script-2.result.rows, not$.script-2.rows. Get this wrong and the reference resolves to null silently — no error, just empty data downstream. - spel has four
node_treetypes, and onlyjson_pathrepeats the full path inexpression.project_varandflow_variablepaths are relative ($.config.x.ypairs with pathx.y), which looks like a bug and is not. $.config.*project variables let you keepapp_token/table_idout of the flow file entirely — the value lives in AnyCross project settings, so the same JSON moves between environments unedited.- Non-ASCII is not what breaks imports. A widely repeated claim says Vietnamese text or emoji cause
Unable to parse uploaded file. It's false — AnyCross's own UI exports are full of raw non-ASCII. The real constraint is valid UTF-8; the usual culprit is writing the file through a locale codec (cp1252 on Windows).ensure_ascii=Trueis still a good default, but as a cheap safeguard, not a taboo. - LarkBase read shapes are not write shapes. A Person field must be passed back as the raw object array, not an extracted name. A URL field needs
{link, text}— a plain string returnsURLFieldConvFail.
More in references/known-errors.md, each entry written as a reusable class of mistake.
The errors below are the ones that cost real hours, with the actual cause rather than the folklore. Full ledger, with the reasoning behind each, in references/known-errors.md.
Your flow.json is not valid UTF-8. On Windows the usual culprit is open(path, "w") without encoding=, which writes through the cp1252 locale codec and mangles any non-Latin text.
It is not caused by non-ASCII characters, despite that claim being widely repeated. AnyCross's own UI exports are full of raw Vietnamese and emoji. Write with encoding="utf-8", or dump with ensure_ascii=True if you want the codec question to never arise.
You wrote a plain string into a LarkBase URL field. It needs an object:
{link: "https://example.com", text: "Open"} // even when link and text are identicalYou read the Person field, ran it through extract() into a display name, and wrote the string back. LarkBase wants the object array it gave you — pass f['<PersonField>'] || [] through untouched. Read shapes and write shapes are not the same thing.
No error, just empty data downstream. Three usual causes:
- Missing
.result— script node output is namespaced. A handler returning{rows: [...]}inscript-2is$.script-2.result.rows, not$.script-2.rows. expressionandnode_tree.pathdisagree because only one was edited. Generate both from one variable.- The referenced node ID doesn't exist — a typo, or renumbering during a rewrite.
validate_flow.py catches (2) and (3).
The parameter key is js_code, not code.
Either the operation ID belongs to the other Bitable connector generation (v2.0 and v2.7 have different IDs and different parameter names for the same action), or your filter has an empty conditions array — which means "no filter, return everything", not "match nothing".
The bot behind the credential is not a member of the target chat. No amount of JSON fixing helps — add the bot to the group in Lark.
The skill is explicit about its own confidence, and so is this README:
- Verified against real exports: Bitable (both connector generations), script, http-client GET, cronjob, webhook, im (Lark cards), card_helper, contact, subflow, loop/branch/while, variable, delay, terminate.
- Unverified — inherited from an earlier version and never confirmed against a real export:
http-clientPOST/PUT operation IDs, Google Docs, Google Drive, and the Zoho Sign patterns.
If you have an export using any of the unverified connectors, an issue or PR with the inspect_export.py output would firm those up for everyone.
The most useful contribution is a connector ID from a tenant this skill hasn't seen, or a new entry in known-errors.md. Two conventions keep that file useful:
- Record the class, not the incident. "Wrote a plain string to a URL field →
URLFieldConvFail" helps everyone; "fixed the link on the booking flow" helps nobody. - No tenant data. No app_tokens, table IDs, credential IDs, chat IDs, or real record IDs — use
<app_token>placeholders. This file gets shared.
MIT — use it, fork it, modify it, ship it in your own work, commercially or not. Just keep the copyright notice. No warranty.
Stuck on a flow, or want one built for you? Get in touch — phucvn2019@gmail.com
I build automation and analytics for SMEs: Lark/Feishu (AnyCross, Lark Base), n8n, and BI/reporting — mostly around supply chain and operations, where the flow has to survive contact with how people actually work, not just pass a demo.
— Phuc