Skip to content

Writing a skill (@zio_skill)

How to Write and Run Skills

This section answers the questions of how to write and how to run. What you can call (Zio.node, Zio.text, Zio.utils …) is documented separately by topic; here we cover where to place that function and how to execute it. No matter which one you use, the placement and execution path are the same.

Write a Python script inside the platform’s .skills/ directory and attach the @zio_skill decorator. The decorated function becomes both a skillFunction node and an AI tool. You do not need to restart the entire platform—just restart that single skill (see “Restart” below).

You do not need to write main.py!

By writing only the @zio_skill function, the platform will automatically generate a main.py during cold start and create a virtual environment (.venv). Do not create either of those yourself.

You can create three versions with the same folder structure. What you build is determined by who calls it.

Form Caller Placed in Folder
Function only Agent node · other skills · curl Put @zio_skill only in *.py. The platform creates main.py.
Server renders the screen Browser (HTML is generated by the server) Write your own main.py — a FastAPI route returns an HTMLResponse.
Browser renders the screen Browser (index.html fetches data) Write your own main.py + index.html. See the Hosting documentation for details.

If you want to use a hand‑crafted main.py, delete the line at the top of that file that says @zio_skill decorator function auto‑registration. While that line is present, the platform rewrites main.py every time it launches the skill — any changes will be overwritten. After removing that line, do not touch it again.

Create one folder under .skills/ and write the @zio_skill function in a .py file inside it; that’s all. One folder is one deployment unit (one virtual environment).

.skills/
└── <폴더명>/ ← 배포 단위 하나
├── <아무이름>.py ← @zio_skill 함수를 여기 적는다 (여러 개 가능)
├── requirements.txt ← (선택) 추가 라이브러리
├── main.py ← ⚡ 플랫폼 자동 생성. 직접 만들지 말 것
└── .venv/ ← ⚡ 플랫폼 자동 생성. 직접 만들지 말 것

The requirements.txt is read only once when you first create the virtual environment. (pandas, fastapi, uvicorn, and requests are installed automatically even if they’re not listed.) Adding a line later will not install it automatically—what to do then is described in the section below titled 「If you added a library later」.

After dragging the skillFunction node onto the canvas in Agent Builder, enter the skill path in the Skill Path field of the right‑hand properties panel using the format folderName/functionName:

my_custom_skills/get_random_greeting
my_custom_skills/get_customer_issue_summary

The skillFunction automatically injects the necessary data into function parameters at runtime based on their names.

Reserved keyword (parameter name) Description and Usage
input (dict) The result returned by the previous node in the workflow canvas is automatically inserted. Example: user_name = input.get("username")
state (dict) The entire workflow state dictionary managed by the LangGraph engine is provided as a whole. Useful for inspecting all context from previous flows.
Shared state variable Variables that were merged into the output space by the previous skill node as part of its execution result. If a function parameter name matches a state key, the value is automatically injected at runtime.
additional_parameter (str) The setting value (text or JSON string) entered in the additional_parameter field of the Agent Builder’s properties panel is injected unchanged.
from zio_ontology import zio_skill, Zio
@zio_skill
def get_customer_issue_summary(input: dict = None, state: dict = None, customer_id: str = None) -> dict:
"""특정 고객의 현재 열려있는(OPEN) 불만 접수 내역을 요약하여 반환합니다."""
cid = customer_id or (input.get("customer_id") if input else None)
if not cid:
return {"error": "고객 ID가 제공되지 않았습니다."}
df = (
Zio.node("Customer")
.where(id=cid)
.out("FILED")
.node("Issue")
.where(status="OPEN")
.fetch()
.to_pandas()
)
if df.empty:
return {"summary": "현재 접수된 불만 내역이 없습니다."}
issue_list = ", ".join(df['title'].tolist())
return {
"open_issues_count": len(df),
"issue_titles": issue_list,
"summary": f"고객님은 현재 {len(df)}건의 미해결 이슈({issue_list})를 가지고 있습니다."
}

A skill is a folder, not a file

A single .py placed directly under .skills/ is not a skill. The platform must have a folder so it can create main.py, a virtual environment (.venv), and plug in the SDK path to launch the process.

Running without the folder results in ModuleNotFoundError: No module named 'zio_ontology'. The code is not wrong; you just skipped the setup steps. Moving it into a folder fixes everything.

Python is not installed on the SSH container. Neither python --version nor import zio_ontology will work here — the absence is normal, so there’s no need to investigate. This is the place to write code; the code runs in the skill runner container. Python, the virtual environment, and the zio_ontology module all exist there.

When you put it in a folder, three different paths appear. All three call the same function — no matter which path you use, the result is identical.

There is only one address. You cannot call it by port

A skill runs inside another container called the skill runner. From an SSH session, that container is invisible — localhost or api:<port> will not open. Do not include a port number in the address. Wherever you see it, that number only has meaning inside the runner.

Where you call it Address
My laptop browser The platform URL unchanged — https://<platform-address>/skills/<folder-name>/docs
Not in SSH (terminal/IDE AI) http://api:8000/api/artifacts/skills/call/<folder-name>/execute/<function-name> — this is always the address. You cannot reach it from an external URL (https://…).
Calling path When to use
/skills/<folder-name>/docs Open in a browser to see the function list and click to run immediately (Swagger). Single‑shot tasks like creating a table are fastest via this path. It can also be opened from the left Skills tab.
POST /execute/<function-name> When calling with curl or another program. Arguments are placed as top‑level keys in the JSON body — the argument name and key name should match. In the browser, /skills/<folder-name> is prefixed; inside SSH, it is prefixed with http://api:8000/api/artifacts/skills/call/<folder-name> (see below).
skillFunction node When adding folder-name/function-name in Agent Builder to insert into a workflow. This is the path used by batch and debugger.

The environment you are connected to via SSH does not have python, python3, or pip. The place where you write code is separate from the place where it runs — you only write files, and execution happens in a different container. Therefore, what follows is not python main.py but a single curl command.

There are two confusing points. The .venv/ folder is visible, but the python inside it is a broken link pointing to another container, and the if __name__ == "__main__": uvicorn.run(...) at the bottom of main.py does not mean you should run it here — the runner starts that file for you. Do not try things like apt install python3. It is unnecessary and will not work.

After fixing, running it is just one line. You start and call it in a single step.

zio run <폴더>/<함수> [JSON] 다시 띄우고 부른다 ← 고친 뒤엔 이것 하나
zio call <폴더>/<함수> [JSON] 부르기만 한다
zio restart <폴더> 다시 띄우기만 한다
zio logs <폴더> [줄수] 안 뜬 까닭 보기
zio deps <폴더> requirements.txt 설치
zio ls 스킬 폴더 목록
zio ping 엔진이 살아 있나
# 예
zio run example_skillFunction/get_recent_customers
zio run my_project/search '{"keyword":"전소영"}'

zio simply wraps the following curl; no new window opens. Data is still read only through the SDK inside the skill. If you want to see what’s happening or if you’re in an environment without zio, use the raw curl below.

The address is always the same. All you need to remember is this one line, and you don’t have to worry about starting it — if it isn’t running, it will start automatically when called. Creating the virtual environment for the first time takes 1–2 minutes, after which responses are immediate.

Do not store the address in a shell variable. IDEs run each command separately. Even if you capture it with URL=$(…) in one command, it won’t persist to the next. Write the full address as shown below.

Replace <folder_name> and <function_name> with your own. These are two different names — the folder is the deployment unit, and the function is the name you wrote after @zio_skill. If in doubt, look it up in the folder (see the third block below).

부른다. 안 떠 있으면 알아서 뜬다 — 이 주소가 전부다

Section titled “부른다. 안 떠 있으면 알아서 뜬다 — 이 주소가 전부다”

curl -s -X POST http://api:8000/api/artifacts/skills/call/<폴더명>/execute/<함수명> \ -H “Content-Type: application/json” -d ‘{}’

코드를 고쳤으면 — 이 줄로 다시 띄운 다음, 위를 그대로 다시 부른다

Section titled “코드를 고쳤으면 — 이 줄로 다시 띄운 다음, 위를 그대로 다시 부른다”

curl -s -X POST http://api:8000/api/artifacts/skills/restart \ -H “Content-Type: application/json” -d ‘{“skill_name”:“<폴더명>”}’

함수 이름이 헷갈리면 — @zio_skill 바로 아래 def 이름이 그것이다

Section titled “함수 이름이 헷갈리면 — @zio_skill 바로 아래 def 이름이 그것이다”

grep -rn -A1 “@zio_skill” /agent_skills/<폴더명>/*.py

>
> > **After fixing, you must restart.** A skill stays running once started; even if the file changes, it will not automatically reload.
>
> > **This applies only to Python code (`.py`).** Static files like `index.html`, CSS, and JS are read from disk on each request, so a browser refresh shows updates without restarting. If you only changed front‑end assets, do not call `restart` — restart is needed only when the Python function itself changes.
>
> > **Conversely, if nothing changed, do not restart.** `restart` kills the running instance and starts a new one — doing it every time you call the function will slow things down because each call spawns a new process. The address stays the same; just call it normally.
>
> - **Inside SSH** — this is the `restart` line in the “Run Inside” section above. If running, it kills and restarts.
> - **From the web UI** — click the skill tab on the left to restart that skill.
> - **Opening `/skills/<folder_name>/docs` in a browser does not restart.** It only starts if nothing is running; if it’s already running, the old code will keep responding.
>
> If startup fails, the reason is logged in `<folder_name>.skill_stderr.log`. In SSH you can view it with:
> `curl -s "http://api:8000/api/artifacts/skills/logs?skill_name=<folder_name>"`
>
> **Starting does not mean your function is registered.** A syntax error will cause the restart itself to fail, but if you omitted `@zio_skill` or made a typo in the function name, the process will start fine and you’ll get a `404` when calling it. That’s why the restart response includes the **currently registered functions** (`loaded_functions`, showing name, signature, first‑line description). Check there first to see if your intended name is present. If none are listed, a `warning` explains why (files starting with `_` or `main` are ignored.)
## If you added libraries later
`requirements.txt` is read once when creating a virtual environment for the first time.
To install newly added packages into an existing virtual environment, run this single line and then restart the skill.

curl -s -X POST http://api:8000/api/artifacts/skills/install-dependencies -H “Content-Type: application/json” -d ‘{“skill_name”:“<폴더명>”}’

If you receive `{"status":"success", …}` then the installation succeeded. **If it takes more than 90 seconds, it will time out** — for heavy libraries, it's safer to delete the entire `.venv` folder and start over (install from scratch).