Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Add new safe-output message types and wire validation/rendering.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 53% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 62% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 17% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 84% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 57% | 0% |
Use this guide to add a new message type to the safe-output messages system so it works in frontmatter, compiler parsing, JavaScript, and bundling.
The messages system lets workflow authors customize safe-output messages. Message flow:
Add the new message field to pkg/parser/schemas/main_workflow_schema.json in the messages object:
json{ "messages": { "properties": { "my-new-message": { "type": "string", "description": "Description of when this message is used. Available placeholders: {placeholder1}, {placeholder2}.", "examples": [ "Example message with {placeholder1}" ] } } } }
Key points:
kebab-case for the YAML field name (e.g., my-new-message)make build after changes (schema is embedded in binary)Add the new field to SafeOutputMessagesConfig in pkg/workflow/compiler.go:
gotype SafeOutputMessagesConfig struct { // ... existing fields ... MyNewMessage string `yaml:"my-new-message,omitempty" json:"myNewMessage,omitempty"` // Description of the message }
Key points:
CamelCase for Go field namekebab-case for YAML tag (matches frontmatter)camelCase for JSON tag (used in JavaScript)omitempty to both tagsIf needed, update the parser in pkg/workflow/safe_outputs.go:
gofunc parseMessagesConfig(messagesMap map[string]any) *SafeOutputMessagesConfig { config := &SafeOutputMessagesConfig{} // ... existing parsing ... if myNewMessage, ok := messagesMap["my-new-message"].(string); ok { config.MyNewMessage = myNewMessage } return config }
Note: The parser uses reflection for most fields, so this step may not be needed for simple string fields.
Create a new file pkg/workflow/js/messages_my_new.cjs:
javascript// @ts-check /// <reference types="@actions/github-script" /> /** * My New Message Module * * This module provides the my-new-message generation * for [describe when it's used]. */ const { getMessages, renderTemplate, toSnakeCase } = require("./messages_core.cjs"); /** * @typedef {Object} MyNewMessageContext * @property {string} placeholder1 - Description of placeholder1 * @property {string} placeholder2 - Description of placeholder2 */ /** * Get the my-new-message, using custom template if configured. * @param {MyNewMessageContext} ctx - Context for message generation * @returns {string} The generated message */ function getMyNewMessage(ctx) { const messages = getMessages(); // Create context with both camelCase and snake_case keys const templateContext = toSnakeCase(ctx); // Default message template const defaultMessage = "Default message with {placeholder1} and {placeholder2}"; // Use custom message if configured return messages?.myNewMessage ? renderTemplate(messages.myNewMessage, templateContext) : renderTemplate(defaultMessage, templateContext); } module.exports = { getMyNewMessage, };
Key points:
messages_<category>.cjs (flat structure, not subfolder)./messages_core.cjs for shared utilitiesCreate pkg/workflow/js/messages_my_new.test.cjs:
javascriptimport { describe, it, expect, beforeEach, vi } from "vitest"; // Mock core global const mockCore = { warning: vi.fn(), }; global.core = mockCore; describe("getMyNewMessage", () => { beforeEach(() => { vi.clearAllMocks(); delete process.env.GH_AW_SAFE_OUTPUT_MESSAGES; }); it("should return default message when no custom message configured", async () => { const { getMyNewMessage } = await import("./messages_my_new.cjs"); const result = getMyNewMessage({ placeholder1: "value1", placeholder2: "value2", }); expect(result).toBe("Default message with value1 and value2"); }); it("should use custom message when configured", async () => { process.env.GH_AW_SAFE_OUTPUT_MESSAGES = JSON.stringify({ myNewMessage: "Custom: {placeholder1}", }); const { getMyNewMessage } = await import("./messages_my_new.cjs"); const result = getMyNewMessage({ placeholder1: "test", placeholder2: "ignored", }); expect(result).toContain("Custom: test"); }); });
Run tests with make test-js.
Add the new property to the SafeOutputMessages typedef in pkg/workflow/js/messages_core.cjs:
javascript/** * @typedef {Object} SafeOutputMessages * @property {string} [footer] - Custom footer message template * // ... existing properties ... * @property {string} [myNewMessage] - Custom my-new-message template */
Also update the getMessages() function return object:
javascriptreturn { footer: rawMessages.footer, // ... existing fields ... myNewMessage: rawMessages.myNewMessage, };
Add the re-export to pkg/workflow/js/messages.cjs:
javascript// Re-export my new messages const { getMyNewMessage } = require("./messages_my_new.cjs"); module.exports = { // ... existing exports ... getMyNewMessage, };
Add to pkg/workflow/js.go:
go//go:embed js/messages_my_new.cjs var messagesMyNewScript string
Add to GetJavaScriptSources():
gofunc GetJavaScriptSources() map[string]string { return map[string]string{ // ... existing entries ... "messages_my_new.cjs": messagesMyNewScript, } }
Import directly from the specific module in scripts that need it:
javascriptconst { getMyNewMessage } = require("./messages_my_new.cjs"); // Use the message const message = getMyNewMessage({ placeholder1: actualValue1, placeholder2: actualValue2, });
Update scratchpad/safe-output-messages.md:
Update the Message Module Architecture table:
markdown| Module | Purpose | Exported Functions | |--------|---------|-------------------| | `messages_my_new.cjs` | My new message description | `getMyNewMessage` |
Before committing:
pkg/parser/schemas/main_workflow_schema.jsonpkg/workflow/compiler.gopkg/workflow/safe_outputs.gopkg/workflow/js/messages_my_new.cjspkg/workflow/js/messages_my_new.test.cjsmessages_core.cjsmessages.cjsjs.goGetJavaScriptSources() mapscratchpad/safe-output-messages.mdmake test-jsmake buildmake lint| File | Purpose | Changes Needed | |------|---------|----------------| | pkg/parser/schemas/main_workflow_schema.json | JSON Schema | Add field definition | | pkg/workflow/compiler.go | Go struct | Add struct field | | pkg/workflow/safe_outputs.go | Parser | Add parsing logic (if needed) | | pkg/workflow/js/messages_my_new.cjs | JavaScript module | Create new file | | pkg/workflow/js/messages_my_new.test.cjs | Tests | Create new file | | pkg/workflow/js/messages_core.cjs | Core utilities | Update typedef | | pkg/workflow/js/messages.cjs | Barrel file | Add re-export | | pkg/workflow/js.go | Go embeddings | Add embed directive | | scratchpad/safe-output-messages.md | Documentation | Document new message |
close-older-discussion MessageThis message type was added following this process:
close-older-discussion field with placeholders {new_discussion_number}, {new_discussion_url}, {workflow_name}, {run_url}CloseOlderDiscussion string fieldmessages_close_discussion.cjs with getCloseOlderDiscussionMessage()GetJavaScriptSources()close_older_discussions.cjs via direct importSee these files for a working implementation example.
Other measured skills in the registry, with their headline benchmark lift.