You’ll receive an email confirming your submission.
Our team will contact you within 24–72 hours, depending on the complexity of your request.
By submitting, you agree to our [Privacy Policy] and consent to receive updates or consultation support from Open Reach Tech.
Please select the privacy consent checkbox.

components..title

components..description

components..title

components..description

You’ll receive an email confirming your submission.
Our team will contact you within 24–72 hours, depending on the complexity of your request.
By submitting, you agree to our [Privacy Policy] and consent to receive updates or consultation support from Open Reach Tech.
Please select the privacy consent checkbox.

Cursor Agent Skills.

Cursor Agent Skills package engineering conventions into Markdown for steadier agent output, shared skill folders in Git, and repeatable workflows.

Banner of Cursor Agent Skills.

Problems When Using AI

  • Regular users often prompt the agent directly to answer questions. However, the same prompt can be interpreted differently depending on the context, which leads to different outputs.

Example:

  • If the current context is about playing, and we prompt with good, the agent will usually understand it as good at playing.
  • If the current context is about studying, and we prompt with good, the agent will usually understand it as good at studying.

=> In many situations, playing and studying are opposite concepts. This is exactly why skills were created — to reduce this kind of ambiguity.


Introduction

  • Skills are knowledge, rules, and workflows that are packaged into files to guide how Cursor Agent works. That is why they are called Cursor Agent Skills.
  • Keep in mind that other agents such as Claude, Codex, etc. also have skills, but since our company uses Cursor, we will focus on Cursor Agent Skills.
  • They are packaged as .md files so agents can use them depending on the context (users can also explicitly specify which skill to use while working with agents).

When to Use Skills

  • Our goal is to standardize the agent’s output across different contexts. Therefore, whenever we have repetitive tasks, rule-based processes, or predefined conventions, we should consider creating a skill.

Examples:

  • Generating code based on conventions
  • Creating tests
  • Creating database seeders

How to Create Good Cursor Agent Skills

Structure

# Skill Name

## Purpose

Explain what this skill is used for.

## When to Use

Describe the situations where the agent should use this skill.

## Instructions

Provide step-by-step guidance.

## Output Format

Define what the final result should look like.

## Examples

Provide examples of good outputs.

Skill Placement

Note: I usually place skills inside Cursor’s configuration directory, but they can also be placed inside the project so the entire team can share them through Git.

.cursor/
  skills/
    technical-design-document.md
    task-breakdown.md
    graphql-resolver.md
    vue-context-pattern.md

What Makes a Good Skill

- Specific
  Avoid vague instructions like “write clean code”.
  Instead, clearly define what “clean code” means in the project.

- Actionable
  Use direct instructions such as “Create a factory method”
  or “Use test.each() for test cases”.

- Structured
  Break instructions into clear sections and steps.

- Example-driven
  Agents follow examples very well.
  If possible, provide both good examples and bad examples.

- Reflect the existing codebase
  Skills should represent the actual patterns being used in the project,
  not ideal patterns that do not exist in the codebase.

- Reusable
  Avoid creating skills that only serve one tiny task,
  unless that task is repeated frequently.

Applying This to ORT

Theory

Note: Skills contain packaged knowledge, rules, and workflows. Therefore, every skill should reflect ORT’s coding conventions and rules below.

Use clear naming
Avoid unnecessary abbreviations
One file, one class
Use factory methods
Use meaningful object parameters
Use Context classes for Vue logic
Write complete JSDoc
Avoid “puzzle-style” code
Prefer readable code over clever but hard-to-understand code

Practice

SKILLS

  • I have written several skills and use them heavily in my daily work. Some of them achieve around 90% of my expected output quality, meaning I rarely need to modify the generated result.
  • If the rules or conventions change one day, I only need to update the rules in one place (similar to an AI Workflow template in Chiho).
  • Below are my skills and their expected quality rates (higher expectation means fewer manual fixes):
  1. ort-graphql => generate GraphQL code (90%)
  2. ort-fetcher => generate fetchers (90%)
  3. ort-submitter => generate submitters (90%)
  4. ort-inject
    • When calling this skill, it asks where to inject, which fetcher or submitter to use, etc.
    • It is similar to adding fetchers or submitters into index.vue, but based on context it also generates reactive status variables, error messages, and more.
    • (around 85%)
  5. ort-design
    • This skill helps create UI.
    • It asks how the design should look and where it should be implemented.
    • My recommendation is to run this skill right after ort-inject because it can reuse the context of fetchers, submitters, statuses, reactives, etc.
    • (around 60–65%)
  6. ort-worker
    • I created a workflow to automate steps 1–5.
    • However, you still need to carefully read the user’s request and provide proper context.
    • (less than 50% success rate in my experience, so I do not recommend using it)
    • I strongly recommend executing steps 1–5 manually, one step at a time, for much better results.

Below is an example of the ort-fetcher skill

---
name: ort-fetcher
description: ORT Fetcher class pattern with full template. Use when creating Fetcher classes for GraphQL queries.
---

# ORT Fetcher Guide

## Naming convention

- Query class → `{Entity}Fetcher` (e.g. `IssueFilesFetcher`)

---

## Fetcher Template

```js
/**
 * Fetcher class for {queryName} GraphQL query.
 */
export default class {Entity}Fetcher {
  /**
   * Constructor.
   *
   * @param {{Entity}FetcherParams} params
   */
  constructor ({
    route,         // omit if no route needed

    statusReactive,

    graphqlClientHash,
  }) {
    this.route = route

    this.statusReactive = statusReactive

    this.graphqlClientHash = graphqlClientHash
  }

  /**
   * Factory method.
   *
   * @template {X extends typeof {Entity}Fetcher ? X : never} T, X
   * @param {{Entity}FetcherFactoryParams} params
   * @returns {InstanceType<T>}
   * @this {T}
   */
  static create ({
    route,

    statusReactive,

    graphqlClientHash,
  }) {
    return /** @type {InstanceType<T>} */ (
      new this({
        route,

        statusReactive,

        graphqlClientHash,
      })
    )
  }

  /**
   * get: {queryName} capsule ref
   *
   * @returns {import('vue').Ref<import('~/.../...GraphqlCapsule.js').default>}
   */
  get {queryName}CapsuleRef () {
    return this.graphqlClientHash
      .{queryName}
      .capsuleRef
  }

  /**
   * get: {queryName} launcher hooks
   *
   * @returns {furo.GraphqlLauncherHooks}
   */
  get {queryName}LauncherHooks () {
    return {
      beforeRequest: async payload => {
        this.statusReactive.isFetching{Entity} = true

        return false
      },
      afterRequest: async capsule => {
        this.statusReactive.isFetching{Entity} = false
      },
    }
  }

  /**
   * Generate {queryName} query params.
   *
   * @param {{QueryParams}} params
   * @returns {{QueryInput} | null}
   */
  generate{Entity}QueryParams ({
    pagination,
  }) {
    const entityId = this.extract{RouteEntity}IdFromRoute()

    if (!entityId) {
      return null
    }

    return {
      entityId,
      pagination,
    }
  }

  /**
   * Fetch {queryName} on mounted.
   *
   * @param {{QueryParams}} params
   * @returns {void}
   */
  fetch{Entity}OnMounted ({
    pagination,
  }) {
    const queryParams = this.generate{Entity}QueryParams({
      pagination,
    })

    if (!queryParams) {
      return
    }

    this.graphqlClientHash
      .{queryName}
      .invokeRequestOnMounted({
        variables: {
          input: queryParams,
        },
        hooks: this.{queryName}LauncherHooks,
      })
  }

  /**
   * Fetch {queryName} on event.
   *
   * @param {{QueryParams}} params
   * @returns {Promise<void>}
   */
  async fetch{Entity}OnEvent ({
    pagination,
  }) {
    const queryParams = this.generate{Entity}QueryParams({
      pagination,
    })

    if (!queryParams) {
      return
    }

    await this.graphqlClientHash
      .{queryName}
      .invokeRequestOnEvent({
        variables: {
          input: queryParams,
        },
        hooks: this.{queryName}LauncherHooks,
      })
  }

  /**
   * Extract {routeEntity} id from route.
   *
   * @returns {number | null}
   */
  extract{RouteEntity}IdFromRoute () {
    const {
      {routeParam},
    } = this.route.params

    const idFromRoute = Array.isArray({routeParam})
      ? {routeParam}.at(0)
      : {routeParam}
    const numericId = Number(idFromRoute)

    if (isNaN(numericId)) {
      return null
    }

    return numericId
  }
}

/**
 * @typedef {{
 *   route: ReturnType<typeof import('vue-router').useRoute>
 *   statusReactive: import('vue').Reactive<{
 *     isFetching{Entity}: boolean
 *   }>
 *   graphqlClientHash: {
 *     {queryName}: ReturnType<import('@openreachtech/furo-nuxt').useGraphqlClient>
 *   }
 * }} {Entity}FetcherParams
 */

/**
 * @typedef {{Entity}FetcherParams} {Entity}FetcherFactoryParams
 */

---

## Key rules

- `beforeRequest`: set `isFetching{Entity} = true`, return `false`
- `afterRequest`: set `isFetching{Entity} = false`
- `generate{X}QueryParams()`: extract route params + build input — return `null` if invalid
- `fetch{X}OnMounted()`: sync, calls `invokeRequestOnMounted`, guard null params with early return
- `fetch{X}OnEvent()`: async, calls `invokeRequestOnEvent`
- All JSDoc typedefs go at the bottom of the file
- `{ClassName}FactoryParams` = `{ClassName}Params` unless factory omits DI-created deps (use `Omit<>`)

How to Use Skills for ORT Frontend

Note: Of course, agents can automatically decide which skill fits a context, but in my opinion this is not ideal. Sometimes the agent makes the wrong decision, which wastes a lot of tokens and requires significant fixes afterward.

Because of that, you should understand both your feature and your skills, then explicitly call the appropriate skill for each context. This significantly reduces token usage.


My Workflow for Building a Complete Feature

1. Understand the feature

  • Every team member should do this.
  • When using skills, you should understand the feature carefully so you can decide which skill to use.

2. Get the schema from Backend

Example:

type Query {
  locales: LocalesResult!

  translatedIssue(input: TranslatedIssueInput!): TranslatedIssueResult!
}

type Mutation {
  translateIssue(input: TranslateIssueInput!): TranslateIssueResult!
}

type LocalesResult {
  locales: [Locale!]!
}

type Locale {
  localeId: Int!
  name: String!
}

type TranslatedContent {
  translatedName: String!
  translatedDescription: String!
  translatedAt: String!
}

type TranslatedIssueResult {
  translation: TranslatedContent
}

type TranslateIssueResult {
  translation: TranslatedContent!
}

input TranslatedIssueInput {
  issueId: Int!
  localeId: Int!
}

input TranslateIssueInput {
  issueId: Int!
  localeId: Int!
}

3. Call the skills in order

ort-graphql → ort-fetcher → ort-submitter
  • You should provide the schema above to the agent.

4. Continue with:

ort-inject → ort-design

5. Final editing

  • Always review the generated files carefully.
  • Never assume the AI rendered everything correctly.

Below Is My Skill Pack

(Currently the document system does not support attaching files, so I will attach it to the issue later.)

Download: click to download

Installation

  • Extract the files
  • Drag and drop them into Cursor chat
  • Ask Cursor to install them

Usage

  • In chat, type:
/skill-name

Example:

/ort-fetcher