Skip to content

Zio.Code()

The only entry point that locks the table is this one. To compare it with Net, see the table in Zio.Database (Overview).

Basic Code reads and writes tables managed by people in the screen from skills. The prompt includes a list {{$테이블.필드}} that shows data you see.

Why is this entry point necessary? Basic Code stores JSONB in the data column of the code_rows table. Without this entry point, a skill would have to know that storage structure and write SQL with Zio.raw()—a raw access to the engine’s internal structure—and if the storage method changes, all skills for every instance will break together.

The engine does not know what tables exist. Construction uses 공종, fashion uses 브랜드, food service uses 점포. Both table names and column names are passed only as arguments. Do not put code that assumes a particular table into the SDK or engine.

Method Description
Zio.code(table_name) Specifies one Basic Code table. Use the Table ID (English name) from the Basic Code screen. If you specify a non‑existent table, a LookupError is raised—typos are not silently turned into an empty list.
.where(**kwargs) Filters rows. Uses comparison suffixes as they are. (e.g., use_yn="Y", client_name__startswith="GS")
.where_or(**kwargs) Groups multiple conditions on the same table with OR (different from other .where() which is AND). Use when any one of several columns matches—previously you had to split queries and combine them in Python. Suffixes (__contains, etc.) are the same as .where(). Calling it multiple times creates separate OR groups that are combined with AND. It only applies to read operations (fetch()·count())—applying OR to update()·delete() can affect more rows than intended, so it is disallowed. (e.g., .where_or(client_name__contains=kw, aliases__contains=kw))
Zio.code.list() First checks what tables exist. Returns [{table_name, title, description, field_count}]—no rows are returned. Do not repeatedly guess with Zio.code("guess"); choose from here.
.fetch() Retrieves a list of rows as GraphResult. Each row is a flat dict whose keys are the column names unchanged. The row identifier _id is included.
.info() Gets a dict describing the table itself: {table_name, title, description, fields:[{name, label, type}]}. For tables without a title, title defaults to the table name.
.create(props) Inserts one row. Duplicates are not checked—what is considered duplicate varies by table, so check with .where(...).fetch() before inserting.
.update(props) Updates rows that match the condition. Only the specified columns are overwritten; others remain unchanged. If no condition is given, a ValueError is raised.
.delete() Deletes rows that match the condition. Returns the deleted rows—so you know what was removed. If no condition is given, a ValueError is raised.
.create_table(columns, title, description) Creates a table. columns=[{name, label, type}]. The created table is managed by people in the screen afterward—skills do not lock it. When it is first created there are no locks (the same as a table made in the screen).
.add_column(column) Adds one column at the end. {name, label, type}. Existing rows will show that column empty—values are not inserted.
.drop_column(column_name) Removes a column from the declaration. Values already stored in rows are not deleted—if you add a column with the same name again they reappear. The values disappear from the screen but are still returned by .fetch() (unless read is filtered out). Trying to use a dropped column later raises ValueError. You cannot drop the last remaining column.
.rename_column(old_name, new_name) Renames a column and moves its values along with it. If you only change the declaration, the old key remains in the data, is not deleted, and becomes invisible.
.drop_table(force=False) Deletes the table and all its rows. If other parts of the system reference this table, it refuses and tells you what references it. To delete anyway, set force=True. Returns {table_name, deleted_rows, dependencies}.
.resolve(value, name, alias=None) Checks if this table knows a notation. If yes, returns the official name; otherwise returns None. It only matches exactly 100%—case, whitespace, and punctuation are ignored (GS 25 = GS25).
.suggest(value, name, alias=None, limit=3) Picks similar official names. When .resolve() fails, this provides candidate options for the user. The input includes aliases; the output is only official names.
.name_conflicts(name, alias=None) Reports aliases that belong to two different official names: [{"alias": "편의점", "claimed_by": ["GS25", "CVSNET"]}]. An empty list means no conflicts.

There is a blocked table — PermissionError

Section titled “There is a blocked table — PermissionError”

Locks are written in the table itself. The SDK sees the same values that the screen (Basic Code management) sees—if there are two judges, you can have a gap where “the screen cannot do it but the skill can.”

Field What it locks
is_data_user_editable The entire row. If off, only read (fetch·count·info) is allowed
allowed_actions Which actions are permitted. If ["update"], values can be changed but rows cannot be added or deleted. Empty means all allowed.
is_schema_user_editable Column configuration. Locks add_column·drop_column·rename_column·drop_table.

When blocked, a sentence explaining why it is blocked and what is allowed appears. Read that sentence and stop—creating a new table with the same name or repeating the same call does not solve it. Locks are removed by people in the screen.

PermissionError: 'llm_callers' 표는 삭제를 허용하지 않습니다. 이 표에 허용된 것: 수정.
PermissionError: 'llms' 표는 칸 구성이 잠겨 있습니다(칸 추가 거부).
시스템이 칸 이름으로 읽는 표라, 바뀌면 조회가 조용히 어긋납니다.
행은 잠금과 별개입니다 — 값만 고치려면 create·update·delete 를 쓰십시오.

Locked tables are system tables read by the engine using column names (llms·llm_callers·system_configs·collectors·common_code). Tables created by a manager are not locked—whether made in the screen or set up by a skill, they behave the same.

Which column is the official name and which is an alias is known only to the person who created the table. Construction uses 공종명, fashion uses 브랜드명, and some tables do not have an alias column at all. If the SDK pre‑sets a name like aliases, it becomes industry‑specific, and for tables without an alias column it silently does nothing.

Zio.code("client").resolve("GS편의점", name="client_name", alias="aliases")
# → 'GS25' 별칭에 100% 일치
Zio.code("client").resolve("하나비전", name="client_name", alias="aliases")
# → None 모르는 표기다. 되물을 차례
Zio.code("client").suggest("하나비전", name="client_name", alias="aliases")
# → ['한화비전', '하나시스'] 사람에게 보여줄 후보
# 별칭 칸이 없는 표는 alias 를 안 주면 됩니다 — 정식명만 재료가 됩니다
Zio.code("product").suggest("삼성전자", name="product_name")

There is a reason why .resolve() and .suggest() are not combined into one function. Mixing “known” and “similar” would cause unnecessary or missing prompts. If .resolve() gives an answer, you pass; if not, you get suggestions from .suggest() to ask the user—this two‑step flow is standard.

Do not touch a dictionary that is wrong. When an alias belongs to two official names (e.g., 편의점 for two convenience store companies), which one it attaches to is determined by the order in the table, and no one can later find out why it was chosen. .resolve()·.suggest() never use such aliases, so they are not attached incorrectly, but if you want to know that a dictionary is wrong, call .name_conflicts(). It does not tell you what to do—whether to skip the entire table is a domain decision.

Use .info()’s title in sentences shown to people

Section titled “Use .info()’s title in sentences shown to people”

Reading a table and reading about a table are different. .fetch() gives you a list of clients, while .info() tells you that the table is “major clients.”

# 나쁨 — 사람은 'client' 가 뭔지 모른다
질문 = f"기초코드에 없는 값입니다: '{}'"
# 나쁨 — 담당자가 화면에서 표 이름을 바꿔도 문장은 안 따라온다
질문 = f"주요거래처에 없는 값입니다: '{}'"
# 좋음
title = Zio.code(code_table).info()["title"]
질문 = f"{title}에 없는 값입니다: '{}'. 어느 쪽입니까?"

The Korean name of a table is the one the domain manager attached in the screen. If you write it into a skill, from then on both places will have the same name, and the manager may not understand why their changed name does not appear on the screen.

Reject update·delete without conditions. If reference data disappears all at once, there is no way to recover it. Always use .where().

Reject columns that are not declared. The screen only displays declared columns, so if you accept a value for an undeclared column it will be stored but never visible and later nobody can find it. The error message includes the columns usable in that table.

The owner of Basic Code is the domain manager. A skill adds one line to a table set up by people in the screen; it does not write into its own storage. If you need skills to create or delete tables, use Zio.entity()—that’s for data whose state changes and disappears (job queues, processing history).

Practical Example of Basic Code Usage — Normalizing Notation

Section titled “Practical Example of Basic Code Usage — Normalizing Notation”

Even if a model outputs GS THE FRESH, you want it stored in the graph as GS더프레쉬. Where to collect which notation is only written in Basic Code; the engine does not know those rules.

from zio_ontology import Zio, zio_skill
@zio_skill
def normalize_names(code_table: str, code_field: str,
alias_field: str, node_label: str,
dry_run: bool = True):
"""기초코드의 별칭 칼럼을 읽어 갈라진 노드 표기를 정식 표기로 모은다.
표 이름·칼럼 이름을 전부 인자로 받으므로 어느 산업에서든 그대로 쓴다.
"""
table = Zio.code(code_table)
# 1) 그래프의 실제 표기를 하나씩 표에 물어본다.
# 사전을 손으로 짜지 않는다 — 별칭 충돌(같은 별칭이 두 정식명에 걸린 것)을
# 거르는 것도 표기 열쇠를 만드는 것도 .resolve() 안에 들어 있다.
planned, unknown = [], []
for node in Zio.node(node_label).fetch().data:
name = node.get(code_field)
if not name:
continue
canon = table.resolve(name, name=code_field, alias=alias_field)
if canon is None:
# 표에 없는 새 값 — 임의로 가까운 이름에 붙이지 않는다.
# 닮은 후보는 사람에게 보여줄 것이지, 코드가 고를 것이 아니다.
unknown.append({
"value": name,
"options": table.suggest(name, name=code_field, alias=alias_field),
})
elif canon != name:
planned.append((name, canon))
# 2) 확인하고 실행한다
if dry_run:
return {"planned": planned, "unknown": unknown, "applied": False}
for name, canon in planned:
Zio.node(node_label).where(**{code_field: name}).consolidate_into(**{code_field: canon})
return {"planned": planned, "unknown": unknown, "applied": True}

Note that dry_run defaults to True. .consolidate_into() cannot be undone, so it’s safest for a skill to first show what will be merged and then execute in two steps.

The options stored in unknown become buttons on the prompt screen. It is fine if the candidate list is empty—then a person can write it directly, put that answer into the alias column, and from next time .resolve() will find it immediately. The prompt ends after one round of questioning.