Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions shortcuts/base/base_shortcuts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -669,9 +669,16 @@ func TestBaseWorkflowHelpGuidesAgents(t *testing.T) {
shortcut: BaseWorkflowUpdate,
wantTips: []string{
"lark-cli base +workflow-update --base-token <base_token> --workflow-id <workflow_id> --json @workflow.json",
"relative path such as --json @./workflow.json",
"pipe it to --json -",
"PUT uses full replacement semantics",
"Use +workflow-get first",
"keep title/status/steps fields",
"explicit update request requires calling this command",
"use +workflow-get to verify every requested field",
"ReminderTrigger offset is directional",
"+N means N units before",
"-N means N units after",
"workflow-id must start with wkf",
"Updating does not enable or disable",
"do not invent steps[].type/data/next/children from natural language",
Expand All @@ -684,6 +691,8 @@ func TestBaseWorkflowHelpGuidesAgents(t *testing.T) {
"workflow-id must start with wkf",
"does not modify steps",
"New workflows are created disabled",
"explicit enable request requires calling this command",
"do not change steps unless the user explicitly requested",
},
},
{
Expand All @@ -692,6 +701,8 @@ func TestBaseWorkflowHelpGuidesAgents(t *testing.T) {
wantTips: []string{
"workflow-id must start with wkf",
"does not delete the workflow or its steps",
"explicit disable request requires calling this command",
"use +workflow-get to verify status",
},
},
}
Expand Down
1 change: 1 addition & 0 deletions shortcuts/base/workflow_disable.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ var BaseWorkflowDisable = common.Shortcut{
Tips: []string{
"workflow-id must start with wkf; do not pass a tbl table ID from the same URL.",
"Disable only changes workflow state; it does not delete the workflow or its steps.",
"An explicit disable request requires calling this command even if list/get already reports disabled; then use +workflow-get to verify status.",
},
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
if strings.TrimSpace(runtime.Str("base-token")) == "" {
Expand Down
2 changes: 2 additions & 0 deletions shortcuts/base/workflow_enable.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ var BaseWorkflowEnable = common.Shortcut{
"workflow-id must start with wkf; do not pass a tbl table ID from the same URL.",
"Enable only changes workflow state; it does not modify steps.",
"New workflows are created disabled; enable after creation only when the user wants it active.",
"An explicit enable request requires calling this command even if list/get already reports enabled; then use +workflow-get to verify status.",
"If enable fails because workflow configuration is incomplete, do not change steps unless the user explicitly requested configuration changes; report the precondition.",
},
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
if strings.TrimSpace(runtime.Str("base-token")) == "" {
Expand Down
5 changes: 4 additions & 1 deletion shortcuts/base/workflow_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,15 @@ var BaseWorkflowUpdate = common.Shortcut{
Flags: []common.Flag{
{Name: "base-token", Desc: "base token", Required: true},
{Name: "workflow-id", Desc: "workflow ID (wkf... prefix)", Required: true},
{Name: "json", Desc: "workflow body JSON; read lark-base-workflow-guide.md and lark-base-workflow-schema.md before replacing steps", Required: true},
{Name: "json", Desc: "workflow body JSON; read lark-base-workflow-guide.md and lark-base-workflow-schema.md before replacing steps", Required: true, Input: []string{common.File, common.Stdin}},
},
Tips: []string{
"lark-cli base +workflow-update --base-token <base_token> --workflow-id <workflow_id> --json @workflow.json",
"JSON files must use a relative path such as --json @./workflow.json; for generated or temporary JSON, pipe it to --json - instead of using @/absolute/path.",
"PUT uses full replacement semantics; omitting steps clears the existing workflow steps.",
"Use +workflow-get first, then edit the returned definition and keep title/status/steps fields you do not intend to change.",
"An explicit update request requires calling this command even if get already shows the requested values; then use +workflow-get to verify every requested field and its normalized meaning.",
"ReminderTrigger offset is directional: +N means N units before (early), while -N means N units after (delayed); translate the value back to natural language before sending the update.",
"workflow-id must start with wkf; do not pass a tbl table ID.",
"Step ids must be unique, and every next/children link must reference an existing step id.",
"Updating does not enable or disable a workflow; call +workflow-enable or +workflow-disable separately.",
Expand Down
2 changes: 2 additions & 0 deletions skills/lark-base/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ metadata:

- Dashboard 的复杂点是 block 的 `data_config`,不是 list/get/create/delete 命令参数。创建或更新 block 前先读 [dashboard-block-data-config.md](references/dashboard-block-data-config.md),组件必须串行创建;`+dashboard-arrange` 是服务端智能布局,仅在用户明确要求重排/美化、或对本次会话从零新建的仪表盘做收尾整理时执行。`+dashboard-block-get-data` 读取图表最终计算结果,不返回 block 名称、类型、布局或 `data_config`;需要元数据先用 `+dashboard-block-get`。
- Workflow 的复杂点是 `steps` 结构。创建、更新或解释完整 workflow 时读入口 [lark-base-workflow-guide.md](references/lark-base-workflow-guide.md) 和 steps JSON SSOT [lark-base-workflow-schema.md](references/lark-base-workflow-schema.md);enable/disable/list 只需确认 workflow ID、当前启停状态和用户意图。
- 用户显式要求 `workflow-update/enable/disable` 时,读取现状只用于定位和构造请求,不能代替写操作:必须执行对应命令,再用 `+workflow-get` 回读目标 workflow 验证结果;即使预读状态或字段已经符合目标,也不要跳过这次明确请求。
- `workflow-update` 只修改用户授权的字段并保留其余定义;方向性配置在写前、回读后都要用自然语言复核。ReminderTrigger 的“提前 N 个单位”必须写正 `offset=N`,“延后 N 个单位”必须写负 `offset=-N`。`workflow-enable` 因业务配置缺失等前置条件失败时,不要擅自修改 steps 绕过校验,继续处理其他对象并报告无法启用项。
- Role 的复杂点是权限 JSON。角色操作先读入口 [lark-base-role-guide.md](references/lark-base-role-guide.md);`+role-create` 只支持自定义角色;`+role-update` 是 delta merge;角色 create/update 或解读完整配置时读权限 JSON SSOT [role-config.md](references/role-config.md)。`+role-delete` 只适用于自定义角色,系统角色不可删除;删除角色和关闭高级权限前必须确认目标和影响。

## 常见恢复
Expand Down
17 changes: 17 additions & 0 deletions skills/lark-base/references/lark-base-workflow-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,23 @@

---

## 修改、启停与验证闭环

用户明确要求修改、启用或停用 workflow 时,采用同一套可审计闭环:

1. 用 `+workflow-list` / `+workflow-get` 定位准确的 `workflow_id` 并读取现状。
2. **始终执行用户要求的写命令。** 预读结果已经符合目标,不代表本次显式请求已完成;`+workflow-enable`、`+workflow-disable` 和保持相同目标值的 `+workflow-update` 都应按请求执行。不要把这条规则扩展到重复 `+workflow-create`。
3. 写入成功后对同一个 `workflow_id` 执行 `+workflow-get`,逐项核对用户要求的状态或字段。不能只以 update/enable/disable 的成功回执作为最终证据。

@yballul-bytedance yballul-bytedance Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里面我感觉应该相信写操作返回的状态?如果 get 一次,其实耗 token 还挺大的,因为 workflow 配置可能比较复杂

4. 回复时把关键返回值翻译回自然语言语义。例如 ReminderTrigger 的 `offset > 0` 表示提前、`offset < 0` 表示延后;写前和回读后都明确复述“提前/延后 N 个单位”,避免只报告裸数字。

`+workflow-update` 是全量替换:从 get 结果构造完整定义,只改变用户授权修改的字段,保留其余 title、status、steps 和节点连线。如果回读字段被服务端丢弃、归一化为不同值或方向与需求不符,不要宣称完成;应返回可验证差异。

`+workflow-enable` 只改变启停状态。若它因缺少接收者、Base App 或其他业务配置而返回前置条件失败,除非用户同时明确授权修改 workflow 配置,否则停止处理该 workflow,不要调用 `+workflow-update` 改 steps 绕过校验;继续启用其余目标,最后报告未启用项和所缺前置条件。

每个 workflow 只有一个首节点 Trigger。多个彼此独立的事件源必须拆成多个 workflow,分别形成 `Trigger → ...Actions` 的连通链;创建后分别回读不同的 `workflow_id`,验证首节点类型、后续链路和启停状态。

---

## 构造技巧

### Loop 构造要点
Expand Down
4 changes: 3 additions & 1 deletion skills/lark-base/references/lark-base-workflow-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,8 @@

### ReminderTrigger

> ⚠️ **先翻译方向,再填写数值:** `offset=N` 表示日期前 N 个单位触发(提前 N);`offset=-N` 表示日期后 N 个单位触发(延后 N)。提交和回读时都按这句自然语言复核,不要只比较绝对值。

Comment on lines +238 to +239

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate target file:"
fd -a 'lark-base-workflow-schema\.md$' . || true

echo
echo "Relevant target section:"
if [ -f skills/lark-base/references/lark-base-workflow-schema.md ]; then
  sed -n '220,270p' skills/lark-base/references/lark-base-workflow-schema.md | cat -n -v
else
  echo "target file missing"
fi

echo
echo "Search ReminderTrigger/offset references in repo:"
rg -n "ReminderTrigger|offset|前 N 个单位|后 N 个单位|at the referenced date|referenced date|referenced date/time|日期前|日期后" skills/lark-base/references/lark-base-workflow-schema.md . --glob '!node_modules' --glob '!dist' --glob '!build' | head -n 200

Repository: larksuite/cli

Length of output: 32160


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Git status/diff summary:"
git diff --stat || true

echo
echo "Exact ReminderTrigger/offset text with line numbers:"
sed -n '236,262p' skills/lark-base/references/lark-base-workflow-schema.md | nl -ba -v236 | sed -e 's/\x0d$//' -e 's/\x00$//'

echo
echo "Related workflow/trigger implementation search:"
rg -n "ReminderTrigger|reminderTrigger|reminder_trigger|offset.*MINUTE|MINUTE|ReminderTrigger|triggerType|trigger" \
  internal shortcuts tests . --glob '!node_modules' --glob '!dist' --glob '!build' --glob '!*.json' | head -n 200

Repository: larksuite/cli

Length of output: 270


Clarify the zero-offset semantics in ReminderTrigger.

MINUTE and DAY both allow offset=0, but the direction rule only defines positive offsets as “before” and negative offsets as “after.” Document that offset=0 means the referenced date/time, or remove it if the service rejects zero.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/lark-base/references/lark-base-workflow-schema.md` around lines 238 -
239, Update the ReminderTrigger offset documentation to explicitly define
offset=0 as triggering at the referenced date/time, while preserving the
existing positive-before and negative-after semantics; only remove zero from the
allowed values if the service rejects it.

```json
{
"table_name": "项目表",
Expand All @@ -252,7 +254,7 @@
| `table_name` | 是 | 数据表名 |
| `field_name` | 是 | 日期字段名(必须为 `datetime` / `created_at` / `formula` / `lookup` 类型) |
| `unit` | 是 | 偏移单位:`MINUTE` / `HOUR` / `DAY` / `WEEK` / `MONTH` |
| `offset` | 是 | 提前/延后的偏移量(正数=提前,负数=延后;范围由 `unit` 决定:`MINUTE` ∈ {0, 5, 15, 30, -5, -15, -30};`HOUR` ∈ [-6, -1] ∪ [1, 6];`DAY` ∈ [-7, 7];`WEEK` ∈ [-7, -1] ∪ [1, 7];`MONTH` ∈ [-7, -1] ∪ [1, 7] |
| `offset` | 是 | 偏移量:正数=日期前/提前,负数=日期后/延后;范围由 `unit` 决定:`MINUTE` ∈ {0, 5, 15, 30, -5, -15, -30};`HOUR` ∈ [-6, -1] ∪ [1, 6];`DAY` ∈ [-7, 7];`WEEK` ∈ [-7, -1] ∪ [1, 7];`MONTH` ∈ [-7, -1] ∪ [1, 7] |
| `hour` | 是 | 触发小时 (0-23),默认 9 |
| `minute` | 是 | 触发分钟 (0-59),默认 0 |
| `condition_list` | 否 | 过滤条件数组,数组中每个元素为 AndCondition 结构,多个 AndCondition 之间为 OR 关系 |
Expand Down
41 changes: 41 additions & 0 deletions tests/cli_e2e/base/base_workflow_update_dryrun_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package base

import (
"context"
"testing"
"time"

clie2e "github.com/larksuite/cli/tests/cli_e2e"
"github.com/stretchr/testify/require"
)

func TestBaseWorkflowUpdateDryRunReadsJSONFromStdin(t *testing.T) {
setBaseDryRunConfigEnv(t)

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
t.Cleanup(cancel)

result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"base", "+workflow-update",
"--base-token", "app_example",
"--workflow-id", "wkf_example",
"--json", "-",
"--dry-run",
},
Stdin: []byte(`{"title":"Updated workflow","status":"OFF","steps":[]}` + "\n"),
DefaultAs: "bot",
})
require.NoError(t, err)
result.AssertExitCode(t, 0)

out := result.Stdout
require.Equal(t, "PUT", clie2e.DryRunGet(out, "api.0.method").String(), out)
require.Equal(t, "/open-apis/base/v3/bases/app_example/workflows/wkf_example", clie2e.DryRunGet(out, "api.0.url").String(), out)
require.Equal(t, "Updated workflow", clie2e.DryRunGet(out, "api.0.body.title").String(), out)
require.Equal(t, "OFF", clie2e.DryRunGet(out, "api.0.body.status").String(), out)
require.True(t, clie2e.DryRunGet(out, "api.0.body.steps").IsArray(), out)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert that steps is empty, not merely an array.

IsArray() would pass if a regression replaced the stdin payload with any non-empty steps array. Assert length zero to verify the exact {"steps":[]} input is preserved.

As per coding guidelines, contract tests must assert the changed field or behavior directly so reverting the implementation causes failure.

Proposed assertion
 require.True(t, clie2e.DryRunGet(out, "api.0.body.steps").IsArray(), out)
+require.Len(t, clie2e.DryRunGet(out, "api.0.body.steps").Array(), 0, out)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
require.True(t, clie2e.DryRunGet(out, "api.0.body.steps").IsArray(), out)
require.True(t, clie2e.DryRunGet(out, "api.0.body.steps").IsArray(), out)
require.Len(t, clie2e.DryRunGet(out, "api.0.body.steps").Array(), 0, out)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/cli_e2e/base/base_workflow_update_dryrun_test.go` at line 40, Update
the assertion on DryRunGet("api.0.body.steps") to verify the steps array has
length zero, rather than only checking IsArray(), while preserving the existing
output context passed to the assertion.

Source: Coding guidelines

}
Loading