From 3004cf673606d155ab1fc733d5656dffa484da46 Mon Sep 17 00:00:00 2001 From: Henrique Hirako Date: Tue, 29 Sep 2026 20:51:00 -0400 Subject: [PATCH 1/6] feat(automations): add created_by dashboard filter to the interface manifest Declare a "Filter by creator" dashboard filter (all / me / others) in automations/interface.json, and admit it in the schema, the TypeScript types, and the manifest tests. The host decides where the filter shows. Refs OpenHands/OpenHands#17816 --- automations/README.md | 13 ++++++----- automations/index.d.ts | 10 +++++++- automations/interface.json | 9 +++++++ automations/interface.schema.json | 39 +++++++++++++++++++++++++++++-- tests/test_interface_manifest.py | 16 +++++++++++++ 5 files changed, 78 insertions(+), 9 deletions(-) diff --git a/automations/README.md b/automations/README.md index 09194c89..4153de10 100644 --- a/automations/README.md +++ b/automations/README.md @@ -270,12 +270,13 @@ falling back to its built-in defaults when the manifest is absent or fails admis captions it: `overview.tiles[].metric` names a host-computed value (`automations`, `needs-attention`, `total-runs`, `average-duration`), filter option values name host predicates (`status`: enabled / latest-run-failed / disabled; `trigger`: `event` matches event-triggered automations, `schedule` - everything else), `sort` values name host comparators, and `insights` captions the host's run-health - states and per-automation stats. The health precedence, the run sampling, the value formatting, the - relative-time rendering, and the filtered-empty state with its reset button are the host's - a manifest - cannot redefine them, only relabel what appears. Tile `detail` copy is plain substitution over the - metric's placeholder namespace (only the `automations` metric exposes `{{active}}`); `zeroDetail` - replaces `detail` while the value is zero. + everything else; `created_by`: `me` matches automations the signed-in user created, `others` everything + else, and the host decides where it is shown), `sort` values name host comparators, and `insights` + captions the host's run-health states and per-automation stats. The health precedence, the run + sampling, the value formatting, the relative-time rendering, and the filtered-empty state with its reset + button are the host's - a manifest cannot redefine them, only relabel what appears. Tile `detail` copy is + plain substitution over the metric's placeholder namespace (only the `automations` metric exposes + `{{active}}`); `zeroDetail` replaces `detail` while the value is zero. - **`docsUrl`** - the automations documentation link, prefix-pinned to docs.openhands.dev by schema. - **`attributes`** - the input surface of an existing Automation: which attributes can be set after creation, keyed by the runtime-model property the host sends (`name`, `prompt`, `model`, `timeout`, diff --git a/automations/index.d.ts b/automations/index.d.ts index 4f61cf58..83815f88 100644 --- a/automations/index.d.ts +++ b/automations/index.d.ts @@ -309,6 +309,7 @@ export type AutomationStatusFilterValue = | "failing" | "disabled"; export type AutomationTriggerFilterValue = "all" | "schedule" | "event"; +export type AutomationCreatedByFilterValue = "all" | "me" | "others"; /** * A filter dropdown. Values name predicates the host implements; the manifest @@ -327,9 +328,16 @@ export interface AutomationTriggerFilter { options: { value: AutomationTriggerFilterValue; label: string }[]; } +export interface AutomationCreatedByFilter { + id: "created_by"; + label: string; + options: { value: AutomationCreatedByFilterValue; label: string }[]; +} + export type AutomationDashboardFilter = | AutomationStatusFilter - | AutomationTriggerFilter; + | AutomationTriggerFilter + | AutomationCreatedByFilter; /** A comparator the host implements, named from a closed set. */ export type AutomationSortValue = "last-run" | "runs" | "name"; diff --git a/automations/interface.json b/automations/interface.json index e89e8211..841748b1 100644 --- a/automations/interface.json +++ b/automations/interface.json @@ -71,6 +71,15 @@ { "value": "schedule", "label": "Scheduled" }, { "value": "event", "label": "Event-driven" } ] + }, + { + "id": "created_by", + "label": "Filter by creator", + "options": [ + { "value": "all", "label": "All creators" }, + { "value": "me", "label": "Created by me" }, + { "value": "others", "label": "Created by others" } + ] } ], "sort": { diff --git a/automations/interface.schema.json b/automations/interface.schema.json index e0461c8b..67d0ea9e 100644 --- a/automations/interface.schema.json +++ b/automations/interface.schema.json @@ -86,12 +86,13 @@ "description": "The filter dropdowns of the list page, in render order. Each filter id may appear once; the host's admission checks that.", "type": "array", "minItems": 1, - "maxItems": 2, + "maxItems": 3, "uniqueItems": true, "items": { "oneOf": [ { "$ref": "#/$defs/statusFilter" }, - { "$ref": "#/$defs/triggerFilter" } + { "$ref": "#/$defs/triggerFilter" }, + { "$ref": "#/$defs/createdByFilter" } ] } }, @@ -425,6 +426,40 @@ } } }, + "createdByFilter": { + "description": "The creator filter. Like the status filter, values name host-implemented predicates: me matches automations the signed-in user created, others every other automation, including one with no recorded creator. The host decides where the filter is shown and may hide it where every automation is the signed-in user's.", + "type": "object", + "additionalProperties": false, + "required": ["id", "label", "options"], + "properties": { + "id": { "const": "created_by" }, + "label": { + "description": "The control's accessible name.", + "$ref": "#/$defs/copy" + }, + "options": { + "description": "Each value may appear once; the host's admission checks that.", + "type": "array", + "minItems": 2, + "maxItems": 8, + "uniqueItems": true, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["value", "label"], + "properties": { + "value": { "enum": ["all", "me", "others"] }, + "label": { "$ref": "#/$defs/copy" } + } + }, + "contains": { + "type": "object", + "required": ["value"], + "properties": { "value": { "const": "all" } } + } + } + } + }, "sort": { "description": "The sort dropdown. Values name comparators the host implements. default must be one of the declared option values; the host's admission checks that, as schemas cannot cheaply.", "type": "object", diff --git a/tests/test_interface_manifest.py b/tests/test_interface_manifest.py index 3bb7b04c..efbd66b1 100644 --- a/tests/test_interface_manifest.py +++ b/tests/test_interface_manifest.py @@ -96,6 +96,22 @@ def test_featured_automations_resolve_to_catalog_entries(): "a status filter without the all option", lambda doc: doc["pages"]["list"]["filters"][0]["options"].pop(0), ), + ( + "a creator filter without the all option", + lambda doc: doc["pages"]["list"]["filters"][2]["options"].pop(0), + ), + ( + "a creator filter value the host does not implement", + lambda doc: doc["pages"]["list"]["filters"][2]["options"].append( + {"value": "team", "label": "My team"} + ), + ), + ( + "more filters than the host renders", + lambda doc: doc["pages"]["list"]["filters"].append( + {**doc["pages"]["list"]["filters"][0], "label": "Filter again"} + ), + ), ], ) def test_schema_rejects(case, mutation): From 88e595ab4f8fd281ba7e25565785dca1678fdebd Mon Sep 17 00:00:00 2001 From: Henrique Hirako Date: Wed, 30 Sep 2026 17:46:08 -0400 Subject: [PATCH 2/6] test(automations): look up the creator filter by id in schema cases The creator-filter cases indexed filters[2]; a reorder would point them at another filter and they would pass without exercising createdByFilter. Refs #708 --- tests/test_interface_manifest.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/test_interface_manifest.py b/tests/test_interface_manifest.py index efbd66b1..b7bff56c 100644 --- a/tests/test_interface_manifest.py +++ b/tests/test_interface_manifest.py @@ -30,6 +30,12 @@ def _with(mutation): return document +def _filter(document, filter_id): + return next( + f for f in document["pages"]["list"]["filters"] if f["id"] == filter_id + ) + + def test_schema_is_a_valid_draft_2020_12_schema(): jsonschema.Draft202012Validator.check_schema(SCHEMA) @@ -98,11 +104,11 @@ def test_featured_automations_resolve_to_catalog_entries(): ), ( "a creator filter without the all option", - lambda doc: doc["pages"]["list"]["filters"][2]["options"].pop(0), + lambda doc: _filter(doc, "created_by")["options"].pop(0), ), ( "a creator filter value the host does not implement", - lambda doc: doc["pages"]["list"]["filters"][2]["options"].append( + lambda doc: _filter(doc, "created_by")["options"].append( {"value": "team", "label": "My team"} ), ), From db9f511287cf5fb7d266cf2f3cb28471b8556fcf Mon Sep 17 00:00:00 2001 From: Henrique Hirako Date: Wed, 30 Sep 2026 17:46:57 -0400 Subject: [PATCH 3/6] feat(automations): label the creator filter "Created by" with Anyone / Me / Others Match the issue's acceptance criteria; under a "Created by" field the options read cleanly without repeating "Created by". Refs #708 --- automations/interface.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/automations/interface.json b/automations/interface.json index 841748b1..beb3c555 100644 --- a/automations/interface.json +++ b/automations/interface.json @@ -74,11 +74,11 @@ }, { "id": "created_by", - "label": "Filter by creator", + "label": "Created by", "options": [ - { "value": "all", "label": "All creators" }, - { "value": "me", "label": "Created by me" }, - { "value": "others", "label": "Created by others" } + { "value": "all", "label": "Anyone" }, + { "value": "me", "label": "Me" }, + { "value": "others", "label": "Others" } ] } ], From 5706e99213a58bb6f5cf02815c985d0a2c889e92 Mon Sep 17 00:00:00 2001 From: Henrique Hirako Date: Thu, 1 Oct 2026 12:31:36 -0400 Subject: [PATCH 4/6] feat(automations): label the creator field "Filter by creator" Match the Status and Trigger fields ("Filter by status", "Filter by trigger") in the same Filters menu. The options stay Anyone / Me / Others. Refs #708 --- automations/interface.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/automations/interface.json b/automations/interface.json index beb3c555..796f08fd 100644 --- a/automations/interface.json +++ b/automations/interface.json @@ -74,7 +74,7 @@ }, { "id": "created_by", - "label": "Created by", + "label": "Filter by creator", "options": [ { "value": "all", "label": "Anyone" }, { "value": "me", "label": "Me" }, From 625435727e499a29cda5e39e67b7d21f5f259f32 Mon Sep 17 00:00:00 2001 From: Henrique Hirako Date: Thu, 1 Oct 2026 12:56:34 -0400 Subject: [PATCH 5/6] test(automations): look up the status filter by id too The status case still indexed filters[0]; use the same _filter helper as the creator cases so every filter case names the filter it changes. Refs #708 --- tests/test_interface_manifest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_interface_manifest.py b/tests/test_interface_manifest.py index b7bff56c..d91d9647 100644 --- a/tests/test_interface_manifest.py +++ b/tests/test_interface_manifest.py @@ -100,7 +100,7 @@ def test_featured_automations_resolve_to_catalog_entries(): ), ( "a status filter without the all option", - lambda doc: doc["pages"]["list"]["filters"][0]["options"].pop(0), + lambda doc: _filter(doc, "status")["options"].pop(0), ), ( "a creator filter without the all option", From fcc7a83da03e3fb9972cfee660331e9962c0d281 Mon Sep 17 00:00:00 2001 From: Henrique Hirako Date: Thu, 1 Oct 2026 15:26:11 -0400 Subject: [PATCH 6/6] docs(automations): drop the no-creator clause from the creator filter The automation service stores a creator for every automation, so others is simply every other automation. Refs #708 --- automations/interface.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/automations/interface.schema.json b/automations/interface.schema.json index 67d0ea9e..77725040 100644 --- a/automations/interface.schema.json +++ b/automations/interface.schema.json @@ -427,7 +427,7 @@ } }, "createdByFilter": { - "description": "The creator filter. Like the status filter, values name host-implemented predicates: me matches automations the signed-in user created, others every other automation, including one with no recorded creator. The host decides where the filter is shown and may hide it where every automation is the signed-in user's.", + "description": "The creator filter. Like the status filter, values name host-implemented predicates: me matches automations the signed-in user created, others every other automation. The host decides where the filter is shown and may hide it where every automation is the signed-in user's.", "type": "object", "additionalProperties": false, "required": ["id", "label", "options"],