Claude Skill

je-custom-content-types

Builds or audits third-party integrations with JetEngine Custom Content Types (CCT): resolving Factory instances, custom-table fields and service columns, Item_Handler create/update/delete hooks, safe queries, Query Builder, related single posts, REST routes and capability bounda

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download lonsdale201-wp-agent-skills-jet-engine_je-custom-content-types-52f6020.zip · 6 KB
Part of lonsdale201/wp-agent-skills — 226 skills

Install

skills CLI npx skills add https://github.com/Lonsdale201/wp-agent-skills/tree/main/jet-engine/je-custom-content-types
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lonsdale201-wp-agent-skills@llmmart
Git git clone https://github.com/Lonsdale201/wp-agent-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole lonsdale201/wp-agent-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

JetEngine Custom Content Types (CCT)

Integrate with registered CCT factories rather than treating CCT rows as posts. Each CCT has a dedicated custom table, its own field schema, service columns, handler lifecycle, query layer, and optional REST/single-post projection.

When to use this skill

  • Read, create, update, or delete items in an existing CCT.
  • React to CCT lifecycle hooks from a companion plugin.
  • Query CCT fields through PHP or Query Builder.
  • Map a CCT item to its optional related single post.
  • Enable or consume jet-cct REST routes.
  • Audit capabilities, ownership, deletion effects, or direct database calls.

Module timing and factory lookup

CCT module construction starts on jet-engine/init; its manager registers saved CCT instances on WordPress init priority 10. Resolve a factory after that point and fail closed when the module/type is absent.

add_action('init', static function(): void {
    if (! class_exists(
        'Jet_Engine\\Modules\\Custom_Content_Types\\Module'
    )) {
        return;
    }

    $module  = \Jet_Engine\Modules\Custom_Content_Types\Module::instance();
    $factory = $module->manager
        ? $module->manager->get_content_types('my_records')
        : false;

    if (! $factory) {
        return;
    }

    // Register integration services that depend on this CCT.
}, 20);

Use get_content_types() with no slug to enumerate factories, but never select a factory from untrusted input without an allowlist.

Data model

CCT records are not WP_Post objects. A table contains _ID, cct_status, cct_author_id, cct_created, cct_modified, optional cct_single_post_id, and configured fields. Use:

$db      = $factory->get_db();
$fields  = $factory->get_formatted_fields();
$item    = $db->get_item($item_id);
$handler = $factory->get_item_handler();

Validate the current item and field schema; CCT administrators can rename or remove fields. Multi-value field types are safely decoded only for known array-backed fields in 3.8.14. Never apply raw unserialize() to CCT values.

Canonical create and update

Use Item_Handler::update_item() for normal mutations. Without _ID it creates; with _ID it updates and merges the existing row before sanitation.

$result = $handler->update_item(array(
    '_ID'        => $existing_id, // omit for create
    'title'      => sanitize_text_field($input['title'] ?? ''),
    'score'      => (int) ($input['score'] ?? 0),
    'cct_status' => 'publish',
));

if (is_wp_error($result)) {
    return $result;
}
if (! $result) {
    return new WP_Error('cct_write_failed', 'CCT write failed.');
}

The handler applies field-type sanitation, timestamps, status allowlisting, related-single processing, and lifecycle hooks. Validate business rules, authorization, required fields, allowed transitions, and foreign IDs before calling it. On create, supply required/default-sensitive fields explicitly; do not assume every configured UI default is inserted by programmatic calls.

The factory DB's output format is mutable and shared within the request. CCT Query Builder sets it to OBJECT, while Item_Handler paths assume array rows. If the same request queried object rows before mutating, normalize and restore:

$db              = $factory->get_db();
$previous_format = $db->get_format_flag();
$db->set_format_flag(\ARRAY_A);
$db->reset_found_items_cache();

try {
    $result = $factory->get_item_handler()->update_item($payload);
} finally {
    $db->reset_found_items_cache();
    $db->set_format_flag($previous_format);
}

Hook only the target slug:

jet-engine/custom-content-types/create-item/{slug}
jet-engine/custom-content-types/created-item/{slug}
jet-engine/custom-content-types/update-item/{slug}
jet-engine/custom-content-types/updated-item/{slug}
jet-engine/custom-content-types/delete-item/{slug}

updated-item/{slug} also fires after creation with an empty previous-item array. Use created-item/{slug} when creation-only behavior is required. These after hooks fire before update_item() performs its final DB-error/result branch, and delete does not verify the low-level delete result before its hook. They are lifecycle notifications, not transaction-commit guarantees. Require a positive item ID, make side effects idempotent, and re-read/queue critical external work after the mutation has been confirmed.

Deletion is a privileged operation

delete_item() enforces the CCT's broad admin capability but can redirect or terminate. raw_delete_item() is the programmatic primitive and performs no access check. Authorize the actor and item before calling it.

if (! current_user_can('delete_others_posts')) {
    return new WP_Error('forbidden', 'Not allowed.');
}

$item = $factory->get_db()->get_item($item_id);
if (! $item || ! my_plugin_user_may_delete_cct($item)) {
    return new WP_Error('forbidden_item', 'Not allowed for this item.');
}

$factory->get_item_handler()->raw_delete_item($item_id);

Raw deletion also permanently deletes cct_single_post_id when present. If delete_item_on_single_delete is enabled, permanently deleting the related single post deletes the CCT row too. Design cleanup hooks for both directions and guard against recursive side effects. In 3.8.14, the low-level delete path does not reset the DB object's in-request _found_items cache. A prior get_item($id) can therefore remain visible on the same instance after the row is gone. When same-request read-after-delete matters, call $factory->get_db()->reset_found_items_cache() after deletion. Also normalize the DB format to ARRAY_A before raw deletion when an earlier CCT query may have selected object output; otherwise the handler can fail before deleting.

Queries

Prefer the CCT DB API or Query Builder type custom-content-type; do not build SQL with table/field/operator request values. Pass filter rows through $factory->prepare_query_args() before $db->query()/count().

$rows = $factory->get_db()->query(
    $factory->prepare_query_args(array(
        array(
            'field'    => 'score',
            'operator' => '>=',
            'value'    => 10,
            'type'     => 'integer',
        ),
    )),
    25,
    0,
    array(array('orderby' => 'score', 'order' => 'DESC', 'type' => 'integer')),
    'AND'
);

Apply explicit limits. Query Builder supports nested groups, search, status, offset/number/order, and returns row objects. Smart Filters may call set_filtered_prop(); protected restrictions must narrow rather than disappear.

REST security boundary

Enabled CCT routes use namespace jet-cct:

GET    /jet-cct/{slug}
GET    /jet-cct/{slug}/{_ID}
POST   /jet-cct/{slug}
POST|PUT|PATCH /jet-cct/{slug}/{_ID}
DELETE /jet-cct/{slug}/{_ID}

An empty capability or literal public makes that operation public. The defaults are public GET and edit_posts for create/update/delete, but only when the corresponding route is enabled. Capability checks are global; they do not enforce item ownership. Add separate object-level policy in your integration when users may only access their own records. Never enable public mutation for private, billable, licensed, or trusted workflow state.

Read cct-query-rest-hooks.md for exact REST parameters, hook signatures, service columns, and query shapes.

Verification

Test create, partial update, invalid status, missing/removed field, array-backed field, create-versus-update hooks, handler error, unauthorized delete, linked single deletion in both directions, zero-result/count queries, paging, public and authenticated REST, per-item ownership, and response-field exposure.

References

  • Official CCT guide: https://crocoblock.com/knowledge-base/articles/jetengine-how-to-create-a-custom-content-type/
  • Verified source paths:
    • wp-content/plugins/jet-engine/includes/modules/custom-content-types/inc/module.php
    • wp-content/plugins/jet-engine/includes/modules/custom-content-types/inc/manager.php
    • wp-content/plugins/jet-engine/includes/modules/custom-content-types/inc/factory.php
    • wp-content/plugins/jet-engine/includes/modules/custom-content-types/inc/item-handler.php
    • wp-content/plugins/jet-engine/includes/modules/custom-content-types/inc/db.php
    • wp-content/plugins/jet-engine/includes/modules/custom-content-types/inc/query-builder/query.php
    • wp-content/plugins/jet-engine/includes/modules/custom-content-types/inc/rest-api/public-controller.php
Files (wp-agent-skills)
  • agents
    • openai.yaml 286 B
      interface:
        display_name: "JetEngine Custom Content Types"
        short_description: "Build safe JetEngine CCT integrations"
        default_prompt: "Use $je-custom-content-types to build or audit a JetEngine Custom Content Type integration with safe CRUD, queries, hooks, and REST permissions."
      
  • references
    • cct-query-rest-hooks.md 4.9 KB
      # CCT query, REST, and hook reference
      
      Load this reference when implementing lifecycle listeners, Query Builder/CCT DB
      filters, related single posts, or a headless client.
      
      ## Lifecycle signatures
      
      ```php
      // Before insert.
      do_action("jet-engine/custom-content-types/create-item/{$slug}", $item, $handler);
      
      // After insert.
      do_action("jet-engine/custom-content-types/created-item/{$slug}", $item, $item_id, $handler);
      
      // Before update.
      do_action("jet-engine/custom-content-types/update-item/{$slug}", $item, $previous, $handler);
      
      // After update, and also after create with $previous = array().
      do_action("jet-engine/custom-content-types/updated-item/{$slug}", $item, $previous, $handler);
      
      // After row and optional linked single post are deleted.
      do_action("jet-engine/custom-content-types/delete-item/{$slug}", $item_id, $item, $handler);
      ```
      
      Global filters include:
      
      ```text
      jet-engine/custom-content-types/item-to-update
      jet-engine/custom-content-types/update-item/sanitize-field-value
      jet-engine/custom-content-types/prepared-query-args
      jet-engine/custom-content-types/user-has-access
      jet-engine/custom-content-types/user-capability
      ```
      
      Scope global filters by `$handler->get_factory()->get_arg('slug')`. A sanitation
      filter receives value, field name, and field definition; it does not receive the
      factory directly.
      
      `created-item` and the create-path `updated-item` fire before the handler checks
      the inserted ID and database error; update-path `updated-item` likewise precedes
      the final error branch. Require a positive ID and do not interpret these hooks
      as a transactional commit signal. The delete hook is also emitted without
      checking the low-level delete result.
      
      ## Service fields and related posts
      
      Core service fields:
      
      ```text
      _ID
      cct_status
      cct_author_id
      cct_created
      cct_modified
      cct_single_post_id (when single-post projection is configured)
      ```
      
      Map a related post back to its row with:
      
      ```php
      $item = $module->manager->get_item_for_post($post_id, $factory);
      ```
      
      Check for `false`/empty return and do not equate the WordPress post ID with CCT
      `_ID`. The linked post is a projection; choose one canonical write path to avoid
      two systems overwriting each other.
      
      After `raw_delete_item()`, reset the factory DB's found-item cache before a
      same-request `get_item()` existence check. The 3.8.14 delete method removes the
      database row but does not invalidate the `_found_items` array populated by an
      earlier read. CCT Query Builder also leaves the shared DB format flag as
      `OBJECT`; restore `ARRAY_A` and reset the cache before `Item_Handler` mutations,
      then restore the caller's previous format in `finally`.
      
      ## CCT DB query rows
      
      Simple row:
      
      ```php
      array(
          'field'    => 'amount',
          'operator' => 'BETWEEN',
          'value'    => array(10, 100),
          'type'     => 'float',
      )
      ```
      
      Nested Query Builder group:
      
      ```php
      array(
          'is_group' => true,
          'relation' => 'OR',
          'args'     => array(/* filter rows */),
      )
      ```
      
      Allowlisted relations are `AND`/`OR`. Operators and types are normalized by the
      CCT DB layer. Still reject unknown field names and set finite limit/offset at
      the integration boundary. In 3.8.10+, an incoming `_ID IN` filter is intersected
      with an existing `_ID IN` restriction instead of replacing it.
      
      Query Builder settings use type slug `custom-content-type` and keys including:
      
      ```text
      content_type, status, args, relation, search_query,
      offset, number, order
      ```
      
      ## REST configuration mapping
      
      The internal names reflect legacy labels and are easy to confuse:
      
      | Operation | Enable setting | Capability setting |
      |---|---|---|
      | list/single GET | `rest_get_enabled` | `rest_get_access` |
      | create POST | `rest_put_enabled` | `rest_put_access` |
      | update POST/PUT/PATCH | `rest_post_enabled` | `rest_post_access` |
      | delete DELETE | `rest_delete_enabled` | `rest_delete_access` |
      
      Permission logic returns true when the capability is empty or `public`; otherwise
      it calls `current_user_can($capability)`. There is no built-in item-owner check.
      
      List parameters include:
      
      ```text
      configured field names
      _cct_search
      _cct_search_by (comma-separated allowed fields)
      _limit
      _offset
      _orderby
      _order
      _ordertype
      _filters (JSON-encoded query/filter structure)
      ```
      
      When `_limit` is positive, response headers include `Jet-Query-Total` and
      `Jet-Query-Pages`. In 3.8.9.1+, search-by fields are intersected with configured
      CCT fields. Do not treat that as object authorization or a response-field
      allowlist.
      
      REST response transforms can be registered through:
      
      ```text
      jet-engine/custom-content-types/rest-api/filters/{slug}
      ```
      
      Query/limit/offset/order filters are:
      
      ```text
      jet-engine/custom-content-types/rest-api/{slug}/get-items/query
      jet-engine/custom-content-types/rest-api/{slug}/get-items/limit
      jet-engine/custom-content-types/rest-api/{slug}/get-items/offset
      jet-engine/custom-content-types/rest-api/{slug}/get-items/order
      ```
      
      Apply finite maximums even if a client requests a larger limit. Strip internal
      columns from public responses and test unauthenticated single-item enumeration.
      
  • SKILL.md 9.2 KB
    ---
    name: je-custom-content-types
    description: >-
      Builds or audits third-party integrations with JetEngine Custom Content Types
      (CCT): resolving Factory instances, custom-table fields and service columns,
      Item_Handler create/update/delete hooks, safe queries, Query Builder, related
      single posts, REST routes and capability boundaries. Use when a plugin reads
      or mutates CCT rows, listens for CCT lifecycle events, exposes CCT data to a
      headless client, or diagnoses missing sanitation, bypassed hooks, unsafe raw
      deletion, public writes, ownership leaks, or CCT/query inconsistencies.
    metadata:
      wp-skills-author: "Soczó Kristóf"
      wp-skills-contact: "mailto:lonsdale201@hotmail.com"
      wp-skills-plugin: "jet-engine"
      wp-skills-plugin-version-tested: "3.8.14"
      wp-skills-wp-version-tested: "7.0.4"
      wp-skills-php-min: "7.4"
      wp-skills-last-updated: "2026-08-17"
    ---
    
    # JetEngine Custom Content Types (CCT)
    
    Integrate with registered CCT factories rather than treating CCT rows as posts.
    Each CCT has a dedicated custom table, its own field schema, service columns,
    handler lifecycle, query layer, and optional REST/single-post projection.
    
    ## When to use this skill
    
    - Read, create, update, or delete items in an existing CCT.
    - React to CCT lifecycle hooks from a companion plugin.
    - Query CCT fields through PHP or Query Builder.
    - Map a CCT item to its optional related single post.
    - Enable or consume `jet-cct` REST routes.
    - Audit capabilities, ownership, deletion effects, or direct database calls.
    
    ## Module timing and factory lookup
    
    CCT module construction starts on `jet-engine/init`; its manager registers saved
    CCT instances on WordPress `init` priority 10. Resolve a factory after that
    point and fail closed when the module/type is absent.
    
    ```php
    add_action('init', static function(): void {
        if (! class_exists(
            'Jet_Engine\\Modules\\Custom_Content_Types\\Module'
        )) {
            return;
        }
    
        $module  = \Jet_Engine\Modules\Custom_Content_Types\Module::instance();
        $factory = $module->manager
            ? $module->manager->get_content_types('my_records')
            : false;
    
        if (! $factory) {
            return;
        }
    
        // Register integration services that depend on this CCT.
    }, 20);
    ```
    
    Use `get_content_types()` with no slug to enumerate factories, but never select
    a factory from untrusted input without an allowlist.
    
    ## Data model
    
    CCT records are not `WP_Post` objects. A table contains `_ID`, `cct_status`,
    `cct_author_id`, `cct_created`, `cct_modified`, optional
    `cct_single_post_id`, and configured fields. Use:
    
    ```php
    $db      = $factory->get_db();
    $fields  = $factory->get_formatted_fields();
    $item    = $db->get_item($item_id);
    $handler = $factory->get_item_handler();
    ```
    
    Validate the current item and field schema; CCT administrators can rename or
    remove fields. Multi-value field types are safely decoded only for known
    array-backed fields in 3.8.14. Never apply raw `unserialize()` to CCT values.
    
    ## Canonical create and update
    
    Use `Item_Handler::update_item()` for normal mutations. Without `_ID` it
    creates; with `_ID` it updates and merges the existing row before sanitation.
    
    ```php
    $result = $handler->update_item(array(
        '_ID'        => $existing_id, // omit for create
        'title'      => sanitize_text_field($input['title'] ?? ''),
        'score'      => (int) ($input['score'] ?? 0),
        'cct_status' => 'publish',
    ));
    
    if (is_wp_error($result)) {
        return $result;
    }
    if (! $result) {
        return new WP_Error('cct_write_failed', 'CCT write failed.');
    }
    ```
    
    The handler applies field-type sanitation, timestamps, status allowlisting,
    related-single processing, and lifecycle hooks. Validate business rules,
    authorization, required fields, allowed transitions, and foreign IDs before
    calling it. On create, supply required/default-sensitive fields explicitly;
    do not assume every configured UI default is inserted by programmatic calls.
    
    The factory DB's output format is mutable and shared within the request. CCT
    Query Builder sets it to `OBJECT`, while `Item_Handler` paths assume array rows.
    If the same request queried object rows before mutating, normalize and restore:
    
    ```php
    $db              = $factory->get_db();
    $previous_format = $db->get_format_flag();
    $db->set_format_flag(\ARRAY_A);
    $db->reset_found_items_cache();
    
    try {
        $result = $factory->get_item_handler()->update_item($payload);
    } finally {
        $db->reset_found_items_cache();
        $db->set_format_flag($previous_format);
    }
    ```
    
    Hook only the target slug:
    
    ```text
    jet-engine/custom-content-types/create-item/{slug}
    jet-engine/custom-content-types/created-item/{slug}
    jet-engine/custom-content-types/update-item/{slug}
    jet-engine/custom-content-types/updated-item/{slug}
    jet-engine/custom-content-types/delete-item/{slug}
    ```
    
    `updated-item/{slug}` also fires after creation with an empty previous-item
    array. Use `created-item/{slug}` when creation-only behavior is required. These
    after hooks fire before `update_item()` performs its final DB-error/result
    branch, and delete does not verify the low-level delete result before its hook.
    They are lifecycle notifications, not transaction-commit guarantees. Require a
    positive item ID, make side effects idempotent, and re-read/queue critical
    external work after the mutation has been confirmed.
    
    ## Deletion is a privileged operation
    
    `delete_item()` enforces the CCT's broad admin capability but can redirect or
    terminate. `raw_delete_item()` is the programmatic primitive and performs no
    access check. Authorize the actor and item before calling it.
    
    ```php
    if (! current_user_can('delete_others_posts')) {
        return new WP_Error('forbidden', 'Not allowed.');
    }
    
    $item = $factory->get_db()->get_item($item_id);
    if (! $item || ! my_plugin_user_may_delete_cct($item)) {
        return new WP_Error('forbidden_item', 'Not allowed for this item.');
    }
    
    $factory->get_item_handler()->raw_delete_item($item_id);
    ```
    
    Raw deletion also permanently deletes `cct_single_post_id` when present. If
    `delete_item_on_single_delete` is enabled, permanently deleting the related
    single post deletes the CCT row too. Design cleanup hooks for both directions
    and guard against recursive side effects. In 3.8.14, the low-level delete path
    does not reset the DB object's in-request `_found_items` cache. A prior
    `get_item($id)` can therefore remain visible on the same instance after the row
    is gone. When same-request read-after-delete matters, call
    `$factory->get_db()->reset_found_items_cache()` after deletion. Also normalize
    the DB format to `ARRAY_A` before raw deletion when an earlier CCT query may
    have selected object output; otherwise the handler can fail before deleting.
    
    ## Queries
    
    Prefer the CCT DB API or Query Builder type `custom-content-type`; do not build
    SQL with table/field/operator request values. Pass filter rows through
    `$factory->prepare_query_args()` before `$db->query()`/`count()`.
    
    ```php
    $rows = $factory->get_db()->query(
        $factory->prepare_query_args(array(
            array(
                'field'    => 'score',
                'operator' => '>=',
                'value'    => 10,
                'type'     => 'integer',
            ),
        )),
        25,
        0,
        array(array('orderby' => 'score', 'order' => 'DESC', 'type' => 'integer')),
        'AND'
    );
    ```
    
    Apply explicit limits. Query Builder supports nested groups, search, status,
    offset/number/order, and returns row objects. Smart Filters may call
    `set_filtered_prop()`; protected restrictions must narrow rather than disappear.
    
    ## REST security boundary
    
    Enabled CCT routes use namespace `jet-cct`:
    
    ```text
    GET    /jet-cct/{slug}
    GET    /jet-cct/{slug}/{_ID}
    POST   /jet-cct/{slug}
    POST|PUT|PATCH /jet-cct/{slug}/{_ID}
    DELETE /jet-cct/{slug}/{_ID}
    ```
    
    An empty capability or literal `public` makes that operation public. The
    defaults are public GET and `edit_posts` for create/update/delete, but only when
    the corresponding route is enabled. Capability checks are global; they do not
    enforce item ownership. Add separate object-level policy in your integration
    when users may only access their own records. Never enable public mutation for
    private, billable, licensed, or trusted workflow state.
    
    Read [cct-query-rest-hooks.md](references/cct-query-rest-hooks.md) for exact REST
    parameters, hook signatures, service columns, and query shapes.
    
    ## Verification
    
    Test create, partial update, invalid status, missing/removed field, array-backed
    field, create-versus-update hooks, handler error, unauthorized delete, linked
    single deletion in both directions, zero-result/count queries, paging, public
    and authenticated REST, per-item ownership, and response-field exposure.
    
    ## References
    
    - Official CCT guide: <https://crocoblock.com/knowledge-base/articles/jetengine-how-to-create-a-custom-content-type/>
    - Verified source paths:
      - `wp-content/plugins/jet-engine/includes/modules/custom-content-types/inc/module.php`
      - `wp-content/plugins/jet-engine/includes/modules/custom-content-types/inc/manager.php`
      - `wp-content/plugins/jet-engine/includes/modules/custom-content-types/inc/factory.php`
      - `wp-content/plugins/jet-engine/includes/modules/custom-content-types/inc/item-handler.php`
      - `wp-content/plugins/jet-engine/includes/modules/custom-content-types/inc/db.php`
      - `wp-content/plugins/jet-engine/includes/modules/custom-content-types/inc/query-builder/query.php`
      - `wp-content/plugins/jet-engine/includes/modules/custom-content-types/inc/rest-api/public-controller.php`
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related