← Back to list

write-action
by coeus-ventures
⭐ 0🍴 0📅 Jan 22, 2026
SKILL.md
name: write-action description: Write server actions following the Epic architecture patterns. Use when creating server-side logic for behaviors, including authentication, validation, and model calls. Triggers on "create an action", "add an action", or "write an action for".
Write Action
Overview
This skill creates server actions that follow the Epic three-layer architecture. Actions belong to the Backend layer and handle authentication, validation, and orchestration of model calls.
Architecture Context
Frontend: Hooks call actions
|
v
Backend: Actions (auth + validation + orchestration)
|
v
Infrastructure: Models (database operations)
Actions:
- Run on the server (Backend layer)
- Check authentication via
getUser() - Validate inputs with Zod
- Call models for data operations
- Return consistent response format
- NEVER access database directly
Action Location and Naming
app/[role]/[page]/behaviors/[behavior-name]/
actions/
[action-name].action.ts
- File names:
kebab-case.action.ts - Function names:
camelCase
Function Specification Format
Follow the Epic Function specification format from docs/Epic.md:
## functionName(input: InputType): ReturnType
[Short description of what the function does]
- Given: [input parameters and assumptions]
- Returns: [value or outcome returned]
- Calls: [direct dependencies - models, integrations]
### Example: [Scenario name]
#### PreDB
[table_name]:
column1, column2
value1, value2
#### PostDB
[table_name]:
column1, column2
value1, value2
new_id, new_val
Implementation Pattern
'use server';
import { getUser } from '@/lib/auth';
import { Model } from '@/shared/models/model-name';
import { z } from 'zod';
const InputSchema = z.object({
name: z.string().min(1).max(100),
});
type Input = z.infer<typeof InputSchema>;
export async function actionName(input: Input) {
try {
// 1. Authentication check
const user = await getUser();
if (!user) {
return { success: false, error: 'Unauthorized' };
}
// 2. Validate input
const validated = InputSchema.parse(input);
// 3. Call model (never direct DB access)
const result = await Model.create({
...validated,
userId: user.id,
});
// 4. Return success response
return { success: true, data: result };
} catch (error) {
console.error('actionName error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'An error occurred',
};
}
}
Response Format
Always return consistent format:
type ActionResponse<T> =
| { success: true; data: T }
| { success: false; error: string };
Key Patterns
1. Authentication First
const user = await getUser();
if (!user) {
return { success: false, error: 'Unauthorized' };
}
2. Input Validation
const validated = InputSchema.parse(input);
// or with safeParse for custom error handling
const result = InputSchema.safeParse(input);
if (!result.success) {
return { success: false, error: result.error.errors[0].message };
}
3. User-Scoped Operations
// Always filter by userId for user-owned resources
const items = await Model.findByUserId(user.id);
4. Error Handling
try {
// operation
} catch (error) {
console.error('actionName error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'An error occurred',
};
}
Constraints
- MUST include
'use server'directive at top - MUST check authentication when required
- NEVER access database directly - use models
- NEVER import React, Jotai, or frontend code
- ALWAYS return consistent response format
- ALWAYS use try/catch with descriptive errors
Example Specification
## createProject(input: CreateProjectInput): Promise<ActionResponse<Project>>
Creates a new project for the authenticated user.
- Given: project name (1-100 chars) and authenticated user with "client" role
- Returns: the newly created project with status "draft"
- Calls: ProjectModel.findByNameAndUser, ProjectModel.create
### Example: Create project successfully
#### PreDB
users:
id, email, role
1, user@example.com, client
projects:
id, user_id, name, status
1, 1, Existing Project, active
#### Steps
* Call: createProject({ name: "New Project" }) as user 1
* Returns: { id: 2, name: "New Project", status: "draft" }
#### PostDB
projects:
id, user_id, name, status
1, 1, Existing Project, active
2, 1, New Project, draft
### Example: Reject duplicate name
#### PreDB
projects:
id, user_id, name
1, 1, My Project
#### Steps
* Call: createProject({ name: "My Project" }) as user 1
* Throws: "Project name already exists"
#### PostDB
projects:
id, user_id, name
1, 1, My Project
Test Generation
Generate test files at [behavior-path]/tests/[action-name].action.test.ts.
Test Structure
import { describe, it, expect } from 'vitest';
import { PreDB, PostDB } from '@/lib/db-test';
import { db } from '@/db';
import * as schema from '@/db/schema';
import { actionName } from '../[action-name].action';
describe('actionName', () => {
it('should [behavior] when [condition]', async () => {
// PreDB -> PreDB
await PreDB(db, schema, {
users: [{ id: '1', email: 'user@example.com' }],
projects: [],
});
// Steps -> Execute
const result = await actionName({ name: 'New Project' });
// Returns -> Assertions
expect(result.success).toBe(true);
expect(result.data?.name).toBe('New Project');
// PostDB -> PostDB
await PostDB(db, schema, {
projects: [{ id: result.data?.id, name: 'New Project', status: 'draft' }],
}, { allowExtraRows: true });
});
});
Translation Rules
| Spec | Test |
|---|---|
| PreDB (CSV) | PreDB(db, schema, { table: [...] }) |
Call: | Action invocation |
Returns: | expect(result).toBe(...) |
Throws: | expect(result.error).toBe(...) |
| PostDB (CSV) | PostDB(db, schema, { table: [...] }) |
Principles
- Test behavior, not implementation
- Use real database (no mocks)
- Start with ONE test (happy path)
Score
Total Score
50/100
Based on repository quality metrics
✓SKILL.md
SKILL.mdファイルが含まれている
+20
○LICENSE
ライセンスが設定されている
0/10
○説明文
100文字以上の説明がある
0/10
○人気
GitHub Stars 100以上
0/15
○最近の活動
3ヶ月以内に更新がある
0/10
○フォーク
10回以上フォークされている
0/5
✓Issue管理
オープンIssueが50未満
+5
✓言語
プログラミング言語が設定されている
+5
○タグ
1つ以上のタグが設定されている
0/5
Reviews
💬
Reviews coming soon