feat: custom proposal templates (authoring + fill + opt-in gating) - #993
Conversation
Reusable templates for arbitrary FunctionCall proposals: a manifest DSL, Visual/Code authoring, a generated fill form, and per-treasury opt-in. ChangePolicy-gated writes; the produced proposal is an ordinary FunctionCall. Includes nt-be CRUD API + tests, nt-fe engine/UI + unit + e2e, docs and an authoring skill. Logging uses tracing to match the backend.
There was a problem hiding this comment.
Pull request overview
Adds an opt-in “Custom Requests / Request Templates” feature that lets DAOs author, store, and fill manifest-driven custom FunctionCall proposal forms (with a visual builder + code editor), plus supporting backend CRUD/flag endpoints and documentation/agent skills.
Changes:
- Implement manifest schema + placeholder substitution + form-schema generation + visual draft/args-tree tooling (with unit tests) for authoring and filing custom templates.
- Add backend storage and CRUD endpoints for
proposal_templates, plus a per-treasurycustom_requests_enabledflag (ChangePolicy-gated) and expanded test scaffolding. - Wire opt-in exposure via Settings → Developer, sidebar navigation (including pinned templates), and an onboarding tour; add docs and distributable skills.
Reviewed changes
Copilot reviewed 56 out of 64 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| skills/trezu-custom-proposal-template/SKILL.md | Portable LLM skill spec to generate template manifest JSON without repo access. |
| skills/trezu-custom-proposal-template/README.md | Installation/usage instructions for the portable skill. |
| .agents/skills/author-custom-proposal-template/SKILL.md | Repo-local agent skill for authoring/editing manifests against Trezu’s validator. |
| docs/CUSTOM_PROPOSAL_TEMPLATES.md | Canonical DSL + architecture + UX documentation for custom proposal templates. |
| CLAUDE.md | Adds a pointer to the new custom templates documentation. |
| nt-fe/features/proposal-templates/types.ts | Frontend type for backend-returned proposal templates. |
| nt-fe/features/proposal-templates/api.ts | Axios client for templates CRUD + custom-requests flag endpoints. |
| nt-fe/features/proposal-templates/hooks/use-proposal-templates.ts | React Query hook to list templates for the current treasury. |
| nt-fe/features/proposal-templates/hooks/use-proposal-template-mutations.ts | React Query mutations + targeted cache invalidation for template CRUD. |
| nt-fe/features/proposal-templates/hooks/use-custom-requests-enabled.ts | React Query hook/mutation for the opt-in custom-requests flag. |
| nt-fe/features/proposal-templates/manifest.ts | Zod manifest validator + placeholder extraction/substitution helpers. |
| nt-fe/features/proposal-templates/manifest.test.ts | Unit tests for manifest validation, placeholder rules, and error formatting. |
| nt-fe/features/proposal-templates/form-schema.ts | Builds RHF/zod validation + default values from a validated manifest. |
| nt-fe/features/proposal-templates/form-schema.test.ts | Unit tests for form-schema validation and default value coercion. |
| nt-fe/features/proposal-templates/error-map.ts | Maps path: message errors to inline-friendly UI strings. |
| nt-fe/features/proposal-templates/error-map.test.ts | Unit tests for error mapping and inline-path routing. |
| nt-fe/features/proposal-templates/build-proposal.ts | Engine: manifest + filled values → SputnikDAO FunctionCall proposal payload. |
| nt-fe/features/proposal-templates/build-proposal.test.ts | Unit tests for args interpolation and proposal construction. |
| nt-fe/features/proposal-templates/draft.ts | Visual-builder draft model + manifest↔draft conversion + field normalization. |
| nt-fe/features/proposal-templates/draft.test.ts | Unit tests for draft round-tripping, lenient hydration, normalization behavior. |
| nt-fe/features/proposal-templates/args-node.ts | Args-tree editor helpers (value typing, dynamic arg detection, dupe keys). |
| nt-fe/features/proposal-templates/args-node.test.ts | Unit tests for args-node helper behaviors. |
| nt-fe/features/proposal-templates/components/manifest-form.tsx | Renders a validated manifest as a fillable form (RHF + design system). |
| nt-fe/features/proposal-templates/components/template-editor.tsx | Shared create/edit authoring UI with Visual/Code modes and validation gating. |
| nt-fe/features/proposal-templates/components/visual-builder.tsx | Visual authoring UI sections for meta, binding, args-tree, and field config. |
| nt-fe/features/proposal-templates/components/touched-gating.test.tsx | Ensures “no red until touched” behavior for authoring UI. |
| nt-fe/features/onboarding/steps/page-tours.tsx | Adds a one-step tour pointing to the newly revealed sidebar section. |
| nt-fe/features/onboarding/steps/index.tsx | Registers the new onboarding tour. |
| nt-fe/components/animate-ui/icons/code-xml.tsx | Adds an animated “code” icon used for the sidebar entry. |
| nt-fe/components/sidebar.tsx | Adds sidebar section gated by custom-requests flag + pinned templates submenu. |
| nt-fe/app/(treasury)/[treasuryId]/settings/page.tsx | Adds a Developer tab (hidden for guest treasuries) and deep-link support. |
| nt-fe/app/(treasury)/[treasuryId]/settings/components/developer-tab.tsx | Developer tab card to enable/disable Custom Requests + fire onboarding tour. |
| nt-fe/app/(treasury)/[treasuryId]/custom-templates/layout.tsx | Layout/metadata for the Request Templates section. |
| nt-fe/app/(treasury)/[treasuryId]/custom-templates/page.tsx | Templates index page (list, pin/unpin, delete, create/edit navigation). |
| nt-fe/app/(treasury)/[treasuryId]/custom-templates/create/page.tsx | Create-template page using TemplateEditor + create mutation. |
| nt-fe/app/(treasury)/[treasuryId]/custom-templates/[slug]/page.tsx | Per-template fill-and-file page (build + submit proposal). |
| nt-fe/app/(treasury)/[treasuryId]/custom-templates/[slug]/edit/page.tsx | Edit/delete page using TemplateEditor + update/delete mutations. |
| nt-be/migrations/20260619000001_create_proposal_templates.sql | Creates proposal_templates table + indexes + updated_at trigger. |
| nt-be/migrations/20260627000002_add_pinned_to_proposal_templates.sql | Adds pinned column for sidebar pinning behavior. |
| nt-be/migrations/20260627000001_add_custom_requests_enabled.sql | Adds per-treasury custom_requests_enabled flag column. |
| nt-be/src/handlers/proposal_templates.rs | Backend CRUD endpoints + manifest structural validation + tests. |
| nt-be/src/handlers/treasury/custom_requests.rs | Backend GET/PUT for custom-requests enable flag + tests. |
| nt-be/src/handlers/treasury/mod.rs | Registers the new custom_requests treasury handler module. |
| nt-be/src/routes/mod.rs | Wires proposal-templates CRUD + custom-requests flag routes. |
| nt-be/src/handlers/mod.rs | Exposes the new proposal_templates handler module. |
| nt-be/src/handlers/treasury/policy.rs | Extracts shared treasury_policy_cache_key helper for caching/test seeding. |
| nt-be/src/utils/test_utils.rs | Adds test helpers for policy seeding, auth cookies, and request dispatch. |
| nt-be/src/auth/error.rs | Centralizes AuthError → (status,message) mapping for reuse by handlers. |
| nt-be/src/auth/middleware.rs | Adds verify_dao_member_for_http and tests around policy evaluation/cache use. |
| nt-be/src/handlers/address_book.rs | Fixes sqlx nullability override for created_by_wallet + regression test. |
| nt-be/.sqlx/query-fd881f5e05e9494b1ca87f66d411ed04133e0d66df5d949b10f5eb78b0676c37.json | sqlx metadata for proposal_templates INSERT. |
| nt-be/.sqlx/query-f0909d35187099eca523e0e0a0be2547de92116765a206e148e3a80968c975c2.json | sqlx metadata for proposal_templates fetch-by-id (with nullable creator). |
| nt-be/.sqlx/query-e648a256a6492374d9c39511c1ecc63a03f5d653bd018e8a2d90b2951c09081c.json | sqlx metadata for proposal_templates UPDATE. |
| nt-be/.sqlx/query-a999e96f58c9e0f5e0af26d859955ac7c5d65094b52d2944d2551eb735d4861d.json | sqlx metadata for proposal_templates LIST. |
| nt-be/.sqlx/query-498093aafc01d2c8e39728c8edb132571cb34e99964883c0bf99eae3fdbe06f6.json | sqlx metadata for proposal_templates DELETE. |
| nt-be/.sqlx/query-0d0919b0bc0e734a5155a2766337c005bf863105549661b4889b74dc6e60c181.json | sqlx metadata for reading custom_requests_enabled. |
| nt-be/.sqlx/query-3abe05be828d94bd877028f9c5c269ec479c5a5b9dc94a1d1cf254236e6b7382.json | sqlx metadata for updating custom_requests_enabled. |
| nt-be/.sqlx/query-5f205588489df021222df4247d494a6ad21c0216728876d457954378fcaca1a3.json | sqlx metadata update for address_book export nullability override. |
| nt-be/.sqlx/query-3eb239014126a32c3fb4793277485fd23afb943625065102af61fd7432bbfb52.json | sqlx metadata update for address_book list nullability override. |
| nt-be/.sqlx/query-36567cc2245450fcf3089d57db17d3c4a7fc68b261d3081a18728b80eb0142a9.json | sqlx metadata update for address_book list (alternate query) nullability override. |
| nt-be/.sqlx/query-57e6d33d9168ec4314e8db17f256b14eb9cfee654db2842fe5ac72731d3b0df9.json | Removes stale sqlx metadata for a confidential_intents query no longer using offline macros. |
Files not reviewed (8)
- nt-be/.sqlx/query-0d0919b0bc0e734a5155a2766337c005bf863105549661b4889b74dc6e60c181.json: Generated file
- nt-be/.sqlx/query-3abe05be828d94bd877028f9c5c269ec479c5a5b9dc94a1d1cf254236e6b7382.json: Generated file
- nt-be/.sqlx/query-498093aafc01d2c8e39728c8edb132571cb34e99964883c0bf99eae3fdbe06f6.json: Generated file
- nt-be/.sqlx/query-57e6d33d9168ec4314e8db17f256b14eb9cfee654db2842fe5ac72731d3b0df9.json: Generated file
- nt-be/.sqlx/query-a999e96f58c9e0f5e0af26d859955ac7c5d65094b52d2944d2551eb735d4861d.json: Generated file
- nt-be/.sqlx/query-e648a256a6492374d9c39511c1ecc63a03f5d653bd018e8a2d90b2951c09081c.json: Generated file
- nt-be/.sqlx/query-f0909d35187099eca523e0e0a0be2547de92116765a206e148e3a80968c975c2.json: Generated file
- nt-be/.sqlx/query-fd881f5e05e9494b1ca87f66d411ed04133e0d66df5d949b10f5eb78b0676c37.json: Generated file
…review - Translate the whole custom-templates feature (customTemplates namespace, 13 locales) instead of leaving it English-only - Tour copy via PageTourContent/pageTours.requestTemplates (was hardcoded JSX) - Sidebar pinned templates use a real <Link> href, not onClick-only - build-proposal docstring: correct the string-substitution vs typed-args contract - Drop the agent/portable skills; the DSL is maintained in docs/
…review - Translate the whole custom-templates feature (customTemplates namespace, 13 locales) instead of leaving it English-only - Tour copy via PageTourContent/pageTours.requestTemplates (was hardcoded JSX) - Sidebar pinned templates use a real <Link> href, not onClick-only - build-proposal docstring: correct the string-substitution vs typed-args contract - Drop the agent/portable skills; the DSL is maintained in docs/
4218e78 to
f8ad635
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 68 out of 76 changed files in this pull request and generated 5 comments.
Files not reviewed (8)
- nt-be/.sqlx/query-0d0919b0bc0e734a5155a2766337c005bf863105549661b4889b74dc6e60c181.json: Generated file
- nt-be/.sqlx/query-3abe05be828d94bd877028f9c5c269ec479c5a5b9dc94a1d1cf254236e6b7382.json: Generated file
- nt-be/.sqlx/query-498093aafc01d2c8e39728c8edb132571cb34e99964883c0bf99eae3fdbe06f6.json: Generated file
- nt-be/.sqlx/query-57e6d33d9168ec4314e8db17f256b14eb9cfee654db2842fe5ac72731d3b0df9.json: Generated file
- nt-be/.sqlx/query-a999e96f58c9e0f5e0af26d859955ac7c5d65094b52d2944d2551eb735d4861d.json: Generated file
- nt-be/.sqlx/query-e648a256a6492374d9c39511c1ecc63a03f5d653bd018e8a2d90b2951c09081c.json: Generated file
- nt-be/.sqlx/query-f0909d35187099eca523e0e0a0be2547de92116765a206e148e3a80968c975c2.json: Generated file
- nt-be/.sqlx/query-fd881f5e05e9494b1ca87f66d411ed04133e0d66df5d949b10f5eb78b0676c37.json: Generated file
Comments suppressed due to low confidence (1)
nt-fe/components/sidebar.tsx:194
- Add a
useTranslations("customTemplates")instance so the new sidebar section label can be localized (instead of hardcoded English).
| }, | ||
| ] | ||
| : []), | ||
| ...(showDeveloper ? [{ value: "developer", label: "Developer" }] : []), |
| tooltipContent={ | ||
| showLabels | ||
| ? undefined | ||
| : "Request Templates" | ||
| } |
…review - Translate the whole custom-templates feature (customTemplates namespace, 13 locales) instead of leaving it English-only - Tour copy via PageTourContent/pageTours.requestTemplates (was hardcoded JSX) - Sidebar pinned templates use a real <Link> href, not onClick-only - build-proposal docstring: correct the string-substitution vs typed-args contract - Drop the agent/portable skills; the DSL is maintained in docs/
f8ad635 to
d2bb790
Compare
…review - Translate the whole custom-templates feature (customTemplates namespace, 13 locales) instead of leaving it English-only - Tour copy via PageTourContent/pageTours.requestTemplates (was hardcoded JSX) - Sidebar pinned templates use a real <Link> href, not onClick-only - build-proposal docstring: correct the string-substitution vs typed-args contract - Drop the agent/portable skills; the DSL is maintained in docs/
d2bb790 to
ba588dd
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 68 out of 76 changed files in this pull request and generated 2 comments.
Files not reviewed (8)
- nt-be/.sqlx/query-0d0919b0bc0e734a5155a2766337c005bf863105549661b4889b74dc6e60c181.json: Generated file
- nt-be/.sqlx/query-3abe05be828d94bd877028f9c5c269ec479c5a5b9dc94a1d1cf254236e6b7382.json: Generated file
- nt-be/.sqlx/query-498093aafc01d2c8e39728c8edb132571cb34e99964883c0bf99eae3fdbe06f6.json: Generated file
- nt-be/.sqlx/query-57e6d33d9168ec4314e8db17f256b14eb9cfee654db2842fe5ac72731d3b0df9.json: Generated file
- nt-be/.sqlx/query-a999e96f58c9e0f5e0af26d859955ac7c5d65094b52d2944d2551eb735d4861d.json: Generated file
- nt-be/.sqlx/query-e648a256a6492374d9c39511c1ecc63a03f5d653bd018e8a2d90b2951c09081c.json: Generated file
- nt-be/.sqlx/query-f0909d35187099eca523e0e0a0be2547de92116765a206e148e3a80968c975c2.json: Generated file
- nt-be/.sqlx/query-fd881f5e05e9494b1ca87f66d411ed04133e0d66df5d949b10f5eb78b0676c37.json: Generated file
…review - Translate the whole custom-templates feature (customTemplates namespace, 13 locales) instead of leaving it English-only - Tour copy via PageTourContent/pageTours.requestTemplates (was hardcoded JSX) - Sidebar pinned templates use a real <Link> href, not onClick-only - build-proposal docstring: correct the string-substitution vs typed-args contract - Drop the agent/portable skills; the DSL is maintained in docs/
ba588dd to
f0f5f2f
Compare
|
hey @khorolets few UI change requests
|
…review - Translate the whole custom-templates feature (customTemplates namespace, 13 locales) instead of leaving it English-only - Tour copy via PageTourContent/pageTours.requestTemplates (was hardcoded JSX) - Sidebar pinned templates use a real <Link> href, not onClick-only - build-proposal docstring: correct the string-substitution vs typed-args contract - Drop the agent/portable skills; the DSL is maintained in docs/
f0f5f2f to
2291ca0
Compare
| it("stringifies number, boolean, and json values", () => { | ||
| expect( | ||
| interpolateArgs("{{n}}/{{b}}/{{j}}", { | ||
| n: 7, | ||
| b: true, | ||
| j: { k: 1 }, | ||
| }), | ||
| ).toBe('7/true/{"k":1}'); | ||
| }); |
There was a problem hiding this comment.
number / bool member inputs reach the contract as JSON strings. A dynamic input always serializes as "{{key}}" → a string, so a bool toggle yields {"approved": "true"} and a number yields {"count": "5"}. Most contracts will reject these (serde won't coerce "true"→true or "5"→5), which makes the number/bool types unusable for the methods they imply. Was this intentional?
There was a problem hiding this comment.
Oof! That's a catch indeed. Not intentional, fixing. Thank you!
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 68 out of 76 changed files in this pull request and generated 2 comments.
Files not reviewed (8)
- nt-be/.sqlx/query-0d0919b0bc0e734a5155a2766337c005bf863105549661b4889b74dc6e60c181.json: Generated file
- nt-be/.sqlx/query-3abe05be828d94bd877028f9c5c269ec479c5a5b9dc94a1d1cf254236e6b7382.json: Generated file
- nt-be/.sqlx/query-498093aafc01d2c8e39728c8edb132571cb34e99964883c0bf99eae3fdbe06f6.json: Generated file
- nt-be/.sqlx/query-57e6d33d9168ec4314e8db17f256b14eb9cfee654db2842fe5ac72731d3b0df9.json: Generated file
- nt-be/.sqlx/query-a999e96f58c9e0f5e0af26d859955ac7c5d65094b52d2944d2551eb735d4861d.json: Generated file
- nt-be/.sqlx/query-e648a256a6492374d9c39511c1ecc63a03f5d653bd018e8a2d90b2951c09081c.json: Generated file
- nt-be/.sqlx/query-f0909d35187099eca523e0e0a0be2547de92116765a206e148e3a80968c975c2.json: Generated file
- nt-be/.sqlx/query-fd881f5e05e9494b1ca87f66d411ed04133e0d66df5d949b10f5eb78b0676c37.json: Generated file
| const required = field.required === true; | ||
| let schema: z.ZodTypeAny = z | ||
| .string() | ||
| .refine((value) => value !== "" || !required, { | ||
| message: `${field.label} is required`, | ||
| }) |
A member-filled value reached the contract as a string: a `{{field}}`
placeholder always stringified, so a bool toggle serialized as
{"approved":"true"} and a number as {"count":"5"} — which serde-based
contracts reject. That made the `bool` and `number` field types effectively
unusable for the methods they imply.
A lone `{{field}}` bound to a bool/number/json field now resolves to its
typed JSON value (true / 5 / parsed object), while u128 uint/amount and any
composed value stay strings. Covered by build-proposal tests.
Also folds in the review fixes made alongside it:
- validate binding.receiver_id (NEAR account) and method_name (identifier)
in the manifest validator and backend validate_manifest
- drop the RegExp lookbehind in the placeholder matcher (Safari <16.4 throws
at module load); handle escaped {{{{...}}}} via alternation instead
- cap the author regex pattern and the input fed to pattern.test (ReDoS)
- localize the Developer tab, the Request Templates sidebar entry, and
member-facing form-validation messages across all 13 locales
- both sidebar navigations use a real <Link> href
- move the distributable skill from skills/ to docs/
- How It Works empty-state button -> outline; tour copy grammar; wrap the
touched-gating SSR test in an intl provider
frol
left a comment
There was a problem hiding this comment.
It seems to be good enough to get it merged and start testing on staging.





Hello Trezu team! Love the project, great job!
Motivation
My colleague reached me out with the case that is solvable by DAO and I was sure it's a perfect match for Trezu. However, I've figured that Trezu is focused on the payments use-cases and doesn't support the creation of other
FunctionCallproposals. That was a missing piece to be a perfect fit for us.That's why I brought you Custom Proposal Templates [engine].
The improvement that covers advanced use-cases and doesn't affect those who don't care. With this feature enabled any Trezu user can create a form to submit a proposal they need, fully customizable for their use case with close to no limits.
How it works
The sad part, it requires some Domain Specific Language (DSL) to describe the form that will be rendered using the Trezu shared components (natively). As the base for the language I've chosen the familiar JSON and tried to keep the DSL as simple as possible.
The documentation for the feature and DSL is ready for review in the corresponding repository NEAR-DevHub/trezu-docs#9
To avoid poking around with the JSON directly this PR introduces a visual constructor for the custom proposal requests. To cover everyone this PR also contains a distributable skill for the LLM agents so any user can install the skill to whatever they use (claude-code, claude chat, codex, etc.) and ask the LLM to generate the form they need and just paste it.
Notes on backend
Since the feature is built to be native for Trezu, it needs to store data and it does it in the Trezu DB. All the CRUD API, all the tests are in place.
Notes on frontend
As agreed with the Trezu team the feature is opt-in, a new Developer category is introduced in the Settings (ref: #974). All the requirements and recommendations of UI/UX designer have been followed and double-checked.
Unit and playwright e2e tests created.
Demo
I am attaching a recording of the end-to-end demo of the feature. Small note: the designer pointed that all the buttons labels must start each word from the capital letter and the demo was captured before this change. The titles are correct in the PR
demo_apple2.mp4
Notes for reviewers:
CLAUDE.md,docs/,skills/. Just letting you know so it's not a surprise (I've already mentioned about the skill for DSL)P.S.: I've decided to avoid poisoning readers of the PR description with the ai-written one, so it might be not that well-covered. Please let me know if you have any areas you need me to elaborate.