Skip to content

service.json configuration reference

This guide explains how to write service.json for Service Tree. By the end, you can wrap underlying datasets, SQL, or Backend Functions into business commands such as:

text
lovrabet crm customer list
lovrabet crm customer detail 10001
lovrabet crm customer contact list --customer-id 10001

The recommended format is tree-shaped: resources represent business objects, and actions define what you can do with each object. The legacy commands array still works, but keep it only for backward compatibility with older configurations.

Start with the minimal structure

A service configuration typically has four parts:

  1. service: the service entry point, for example crm.
  2. app / apps: which app the service uses by default.
  3. resources: the business objects in the service, such as customers, contacts, and follow-up records.
  4. actions: what can be done with each business object, such as list, detail, and create.

Minimal example:

json
{
  "service": "crm",
  "name": "CRM",
  "description": "客户、联系人和跟进记录的业务服务",
  "app": "app-xxxxxxxx",
  "resources": {
    "customer": {
      "name": "客户",
      "datasetCode": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
      "actions": {
        "list": {
          "description": "查看客户列表",
          "defaults": {
            "currentPage": 1,
            "pageSize": 20,
            "orderBy": [{ "updated_at": "desc" }]
          },
          "flags": {
            "status": "where.status.$eq",
            "keyword": {
              "to": "where.customer_name",
              "op": "$contain",
              "description": "客户名称关键字"
            }
          }
        },
        "detail": {
          "description": "查看客户详情",
          "action": "getOne",
          "args": ["id"],
          "map": {
            "id": {
              "target": "id",
              "transform": "number"
            }
          }
        }
      }
    }
  }
}

This generates two business entry points:

text
lovrabet crm customer list --status active --keyword 科技
lovrabet crm customer detail 10001

They map to:

text
lovrabet data filter --code aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa --params '{"where":{"status":{"$eq":"active"},"customer_name":{"$contain":"科技"}},"currentPage":1,"pageSize":20,"orderBy":[{"updated_at":"desc"}]}'
lovrabet data getOne --code aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa --params '{"id":10001}'

Top-level fields

FieldRequiredDescription
protocolNoProtocol name. Defaults to lovrabet.service-tree/v1.
versionNoConfiguration file version. Defaults to 1.0.0 when omitted.
serviceYesService entry point. A string such as "crm" is recommended; { "code": "crm", "name": "CRM" } is also supported.
name / descriptionNoDisplay name and description. Shown in help, schema, and doctor output.
appNoShortcut for binding a single app. Accepts an appcode or an app name.
appsNoMulti-app binding table. Recommended for new configurations.
appBindingsNoLegacy name with the same semantics as apps, kept for compatibility.
defaultsNoService-level default underlying parameters, inherited by resources and actions.
resourcesRecommendedThe business object tree. Prefer this in new configurations.
commandsLegacyThe legacy flat command array. Still works, but no longer recommended.

service is not an app code. Don't write it as app-xxxx. A service may span multiple apps; service should express the business domain, such as crm, order, or store-ops.

app and apps: binding apps

A single-app service can simply use app:

json
{
  "service": "crm",
  "app": "app-xxxxxxxx"
}

If a service spans multiple apps, use apps:

json
{
  "service": "customer-success",
  "apps": {
    "crm": {
      "appcode": "app-crmxxxx",
      "env": "daily"
    },
    "order": {
      "appcode": "app-orderxxxx",
      "env": "daily"
    }
  }
}

Resources or actions reference an app alias through appRef:

json
{
  "resources": {
    "customer": {
      "appRef": "crm",
      "datasetCode": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
      "actions": { "list": {} }
    },
    "order": {
      "appRef": "order",
      "datasetCode": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
      "actions": { "list": {} }
    }
  }
}

app, apps, and appBindings are only defaults. Apps or environments explicitly specified by the user at command time take precedence.

resources: defining the business object tree

Each key in resources becomes one segment of the command path.

json
{
  "resources": {
    "customer": {
      "name": "客户",
      "resources": {
        "contact": {
          "name": "联系人",
          "actions": {
            "list": {}
          }
        }
      }
    }
  }
}

This generates:

text
lovrabet crm customer contact list

Common resource fields:

FieldDescription
name / descriptionDisplay information for the business object.
appRef / appReferences an app alias from apps / appBindings. Common in multi-app services.
datasetCodeDataset code. The preferred option.
datatable / tablePhysical table name. When datasetCode is absent, the dataset can be resolved by table name.
defaults / paramsResource-level default underlying parameters, such as pagination, sorting, and fixed conditions.
actionsActions on this business object, such as list, mine, detail, and create.
resourcesChild business objects, for deeper command paths.

appRef, datasetCode, datatable, and defaults on a resource are inherited by its child resources and actions. Actions can override these values.

actions: defining business actions

Each key in actions is the last segment of the command path.

json
{
  "actions": {
    "list": {
      "description": "查看客户列表"
    },
    "detail": {
      "description": "查看客户详情",
      "action": "getOne",
      "args": ["id"],
      "map": {
        "id": {
          "target": "id",
          "transform": "number"
        }
      }
    }
  }
}

Common action fields:

FieldDescription
descriptionCommand description.
actionAction shorthand. Defaults to filter. Can be getOne, create, update, delete, or sql.exec, bff.exec.
targetFull target form. Can be an object, or data.filter, sql.exec, bff.exec.
kind / commandExpanded form of target, for example { "kind": "data", "command": "filter" }.
datasetCode / datatable / tableOverrides the dataset locator inherited from the resource.
sqlCodeSQL code. Used with action: "sql.exec".
bffCode / bffId / scriptNameBackend Function locator. Used with action: "bff.exec".
argsPositional arguments. A string shorthand automatically becomes a required argument.
flagsOptional parameters. Object form recommended.
defaults / paramsAction-level default underlying parameters.
map / mapToParameter mapping rules. Use map in new configurations; mapTo in legacy configurations still works.
riskRisk level. Optional for read-only actions; use write for writes; use high-risk-write for deletions or irreversible operations.

Choosing an action form

1. Listing dataset records

When action is omitted, it defaults to data.filter:

json
{
  "resources": {
    "customer": {
      "datasetCode": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
      "actions": {
        "list": {
          "description": "查看客户列表"
        }
      }
    }
  }
}

Generates:

text
lovrabet crm customer list

Maps to:

text
lovrabet data filter --code aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa --params '{}'

2. Getting a single record

json
{
  "actions": {
    "detail": {
      "description": "查看客户详情",
      "action": "getOne",
      "args": ["id"],
      "map": {
        "id": {
          "target": "id",
          "transform": "number"
        }
      }
    }
  }
}

Generates:

text
lovrabet crm customer detail 10001

Maps to:

text
lovrabet data getOne --code aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa --params '{"id":10001}'

3. Running SQL

json
{
  "actions": {
    "stats": {
      "description": "查看客户统计",
      "action": "sql.exec",
      "sqlCode": "customer_stats",
      "flags": {
        "startDate": {
          "to": "startDate",
          "description": "开始日期"
        },
        "endDate": {
          "to": "endDate",
          "description": "结束日期"
        }
      }
    }
  }
}

Generates:

text
lovrabet crm customer stats --start-date 2026-01-01 --end-date 2026-01-31

Maps to:

text
lovrabet sql exec --sqlcode customer_stats --params '{"startDate":"2026-01-01","endDate":"2026-01-31"}'

4. Running a Backend Function

For complex writes, prefer wrapping the logic in a Backend Function so the server handles validation, idempotency, permissions, transactions, and failure recovery.

json
{
  "actions": {
    "create": {
      "description": "创建客户",
      "action": "bff.exec",
      "scriptName": "createCustomer",
      "risk": "write",
      "flags": {
        "customerName": {
          "to": "customerName",
          "required": true,
          "description": "客户名称"
        },
        "industry": {
          "to": "industry",
          "description": "所属行业"
        }
      }
    }
  }
}

Generates:

text
lovrabet crm customer create --customer-name 云兔科技 --industry software

Maps to:

text
lovrabet bff exec --name createCustomer --params '{"customerName":"云兔科技","industry":"software"}'

args: values the caller must supply

args is for values that must be provided at call time, such as a record ID or a customer number.

Shorthand:

json
{
  "args": ["id"]
}

Full form:

json
{
  "args": [
    {
      "name": "id",
      "description": "客户编号",
      "required": true
    }
  ]
}

args only declares what values to collect. To place a value into the underlying request, you also need map:

json
{
  "args": ["id"],
  "map": {
    "id": {
      "target": "id",
      "transform": "number"
    }
  }
}

flags: optional query conditions

flags works well for conditions such as status, owner, keywords, date ranges, and pagination.

Object form recommended:

json
{
  "flags": {
    "status": "where.status.$eq",
    "ownerId": {
      "to": "where.owner_id",
      "op": "$eq",
      "type": "number",
      "transform": "number",
      "description": "负责人 ID"
    },
    "startDate": {
      "to": "where.created_at",
      "op": "$gte",
      "description": "开始日期"
    },
    "pageSize": {
      "to": "pageSize",
      "type": "number",
      "transform": "number",
      "description": "每页条数"
    }
  }
}

On the command line, camelCase is automatically converted to kebab-case:

text
lovrabet crm customer list --owner-id 12 --start-date 2026-01-01 --page-size 50

The above maps to:

json
{
  "where": {
    "owner_id": { "$eq": 12 },
    "created_at": { "$gte": "2026-01-01" }
  },
  "pageSize": 50
}

Flag object fields:

FieldDescription
to / targetMapping target path.
op / operatorOperator, such as $eq, $contain, $gte.
typeParameter type: string, number, boolean, json. Defaults to string when omitted.
transformType conversion: string, number, boolean, json.
descriptionParameter description.
requiredWhether the parameter is required.
defaultDefault value.
enumList of allowed values.
cliNameParameter name shown on the command line. When omitted, it is derived from name in kebab-case.
omitEmptyWhether to skip mapping when the value is empty.

map and mapTo: translating business parameters into underlying parameters

map / mapTo answers one question: where in the underlying request does each business parameter supplied by the caller go?

Use map in new configurations; mapTo from legacy configurations is still supported.

Common sources

SourceDescriptionExample
flags.xxxFrom an optional parameter.flags.status
args.xxxFrom a positional argument.args.id
context.xxx / ctx.xxxFrom the current logged-in user or the runtime context.context.userId
const.xxxFixed value.const.deleted
constFixed-value source.const

In map, a key without a prefix is matched against args or flags automatically. For example, with args: ["id"], "id" is treated as args.id.

Shorthand: write the target path directly

json
{
  "map": {
    "status": "where.status.$eq"
  }
}

This is equivalent to placing the status parameter at:

json
{
  "where": {
    "status": { "$eq": "<status>" }
  }
}

Full form: target, operator, and transform written separately

json
{
  "map": {
    "ownerId": {
      "target": "where.owner_id",
      "operator": "$eq",
      "transform": "number"
    }
  }
}

Business command:

text
lovrabet crm customer list --owner-id 12

Underlying parameters:

json
{
  "where": {
    "owner_id": { "$eq": 12 }
  }
}

Fixed conditions

Fixed conditions express rules such as "show only non-deleted records by default" or "show only a certain type of customer by default".

json
{
  "map": {
    "const.deleted": {
      "target": "where.deleted",
      "operator": "$eq",
      "value": 0,
      "transform": "number"
    }
  }
}

Underlying parameters:

json
{
  "where": {
    "deleted": { "$eq": 0 }
  }
}

Current user

For "my customers", don't hardcode a specific user ID — use the runtime context:

json
{
  "actions": {
    "mine": {
      "description": "查看我负责的客户",
      "map": {
        "context.userId": {
          "target": "where.owner_id",
          "operator": "$eq",
          "transform": "number"
        }
      }
    }
  }
}

Generates:

text
lovrabet crm customer mine

Underlying parameters:

json
{
  "where": {
    "owner_id": { "$eq": "<当前登录用户 ID>" }
  }
}

defaults and params: default underlying parameters

defaults and params both specify default underlying parameters. Use defaults consistently in new configurations; params mainly exists for backward compatibility with older configurations.

They can be written at three levels:

  1. Top-level defaults: inherited by the entire service.
  2. Resource defaults: inherited by this business object and its children.
  3. Action defaults: affects only the current action.

Values merge from the outside in — the closer to the action, the higher the precedence:

text
service.defaults -> resource.defaults -> action.defaults

Example:

json
{
  "defaults": {
    "where": {
      "deleted": { "$eq": 0 }
    }
  },
  "resources": {
    "customer": {
      "defaults": {
        "pageSize": 20
      },
      "actions": {
        "list": {
          "defaults": {
            "currentPage": 1,
            "orderBy": [{ "updated_at": "desc" }]
          }
        }
      }
    }
  }
}

datasetCode and datatable

data actions must be able to locate a dataset.

Prefer datasetCode:

json
{
  "datasetCode": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}

To resolve by physical table name, use datatable or table:

json
{
  "datatable": "crm_customer"
}

Provide at least one of the two. datasetCode is the most explicit and must be a 32-character code. datatable is a good fit when the physical table is identical across environments but dataset codes may differ.

risk: marking the risk level

ValueUse for
readQueries, detail views, statistics. The default.
writeCreating, updating, syncing, triggering business actions.
high-risk-writeDeletions, bulk replacements, irreversible operations.

data actions infer some of the risk automatically:

  • filter, getOne, aggregate default to read
  • create, batchCreate, update default to write
  • delete defaults to high-risk-write

Backend Functions and SQL default to read. If the operation actually writes data, explicitly set risk: "write" or risk: "high-risk-write".

Upgrading from the legacy commands format

Legacy form:

json
{
  "service": {
    "code": "crm",
    "name": "CRM"
  },
  "appBindings": {
    "main": { "appcode": "app-xxxxxxxx" }
  },
  "commands": [
    {
      "path": "customer list",
      "description": "查看客户列表",
      "target": {
        "kind": "data",
        "command": "filter",
        "appRef": "main",
        "datasetCode": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
      },
      "flags": [
        {
          "name": "status",
          "type": "string",
          "mapTo": "where.status.$eq"
        }
      ]
    }
  ]
}

New form:

json
{
  "service": "crm",
  "name": "CRM",
  "apps": {
    "main": { "appcode": "app-xxxxxxxx" }
  },
  "resources": {
    "customer": {
      "appRef": "main",
      "datasetCode": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
      "actions": {
        "list": {
          "description": "查看客户列表",
          "flags": {
            "status": "where.status.$eq"
          }
        }
      }
    }
  }
}

Field mapping:

Legacy fieldNew field
service.codeservice
appBindingsapps
commands[].pathresources path + actions key
commands[].target.datasetCodedatasetCode on the resource or action
commands[].target.commandthe action's action
commands[].flags[]the action's flags object
commands[].mapTothe action's map

The legacy commands array can still be imported and executed. Prefer resources/actions in new configurations: it expresses the business object hierarchy naturally and avoids assembling commands with spaces inside JSON keys or path values.

Complete CRM example

json
{
  "service": "crm",
  "name": "CRM",
  "description": "客户、联系人和跟进记录的业务服务",
  "app": "app-xxxxxxxx",
  "resources": {
    "customer": {
      "name": "客户",
      "datasetCode": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
      "defaults": {
        "currentPage": 1,
        "pageSize": 20
      },
      "actions": {
        "list": {
          "description": "查看客户列表",
          "defaults": {
            "orderBy": [{ "updated_at": "desc" }]
          },
          "flags": {
            "status": "where.status.$eq",
            "keyword": {
              "to": "where.customer_name",
              "op": "$contain",
              "description": "客户名称关键字"
            },
            "page": {
              "to": "currentPage",
              "type": "number",
              "transform": "number"
            },
            "pageSize": {
              "to": "pageSize",
              "type": "number",
              "transform": "number"
            }
          }
        },
        "mine": {
          "description": "查看我负责的客户",
          "map": {
            "context.userId": {
              "target": "where.owner_id",
              "operator": "$eq",
              "transform": "number"
            }
          }
        },
        "detail": {
          "description": "查看客户详情",
          "action": "getOne",
          "args": ["id"],
          "map": {
            "id": {
              "target": "id",
              "transform": "number"
            }
          }
        },
        "create": {
          "description": "创建客户",
          "action": "bff.exec",
          "scriptName": "createCustomer",
          "risk": "write",
          "flags": {
            "customerName": {
              "to": "customerName",
              "required": true,
              "description": "客户名称"
            },
            "industry": {
              "to": "industry",
              "description": "所属行业"
            }
          }
        }
      },
      "resources": {
        "contact": {
          "name": "联系人",
          "datasetCode": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
          "actions": {
            "list": {
              "description": "查看客户联系人",
              "flags": {
                "customerId": {
                  "to": "where.customer_id",
                  "op": "$eq",
                  "type": "number",
                  "transform": "number",
                  "required": true
                }
              }
            }
          }
        }
      }
    }
  }
}

Generated business entry points:

text
lovrabet crm customer list
lovrabet crm customer mine
lovrabet crm customer detail 10001
lovrabet crm customer create --customer-name 云兔科技 --industry software
lovrabet crm customer contact list --customer-id 10001

Validation checklist

Check each item before importing:

  • service is in lowercase kebab-case, for example crm, order, store-ops.
  • There is at least one resources or commands.
  • New configurations prefer resources/actions.
  • Every resource key and action key is in lowercase kebab-case.
  • No two generated full command paths are identical.
  • data actions have a datasetCode or datatable / table.
  • datasetCode is a 32-character code.
  • SQL actions have a sqlCode.
  • Backend Function actions have bffCode, bffId, or scriptName.
  • flags.type only uses string, number, boolean, or json.
  • Sources in map / mapTo only use flags., args., context., ctx., const., or short names that auto-match existing args / flags.
  • Write actions have the correct risk set.
  • In multi-app services, every resource or action appRef can be found in apps.

When to keep using legacy commands

Only two situations justify staying with commands:

  1. You have a large body of existing configuration, only need small fixes in the short term, and don't want to restructure right away.
  2. A service is extremely simple, and your team has confirmed it doesn't need a resource hierarchy.

Prefer resources/actions for new configurations.

基于飞书知识库同步生成,内容以飞书源文档为准