RPC Action Options

View Source

This guide covers all configuration options available for rpc_action declarations, including load restrictions, query controls, identity lookups, and more.

Overview

Each rpc_action accepts two required arguments and optional configuration:

rpc_action :function_name, :ash_action_name, options
ArgumentDescription
FirstName of the generated TypeScript function
SecondName of the Ash action to execute
OptionsKeyword list of configuration options

Load Restrictions

Control which relationships and calculations clients can request using allowed_loads and denied_loads.

allowed_loads (Whitelist)

Only allow loading specific fields:

typescript_rpc do
  resource MyApp.Todo do
    # Only user and tags can be loaded
    rpc_action :list_todos, :read, allowed_loads: [:user, :tags]
  end
end
// Allowed
const result = await listTodos({
  fields: ["id", "title", { user: ["name"], tags: ["name"] }]
});

// Error: "comments" not in allowed_loads
const result = await listTodos({
  fields: ["id", "title", { comments: ["text"] }]
});

denied_loads (Blacklist)

Block specific fields while allowing all others:

typescript_rpc do
  resource MyApp.Todo do
    # Everything except internal_notes can be loaded
    rpc_action :list_todos, :read, denied_loads: [:internal_notes, :audit_log]
  end
end
// Allowed (user is not denied)
const result = await listTodos({
  fields: ["id", "title", { user: ["name"] }]
});

// Error: "internal_notes" is denied
const result = await listTodos({
  fields: ["id", "title", { internal_notes: ["content"] }]
});

Nested Load Restrictions

Restrict nested relationships using keyword list syntax:

typescript_rpc do
  resource MyApp.Todo do
    # Allow user, but only allow loading user's public_profile
    rpc_action :list_todos, :read,
      allowed_loads: [
        :tags,
        user: [:public_profile]
      ]
  end
end
// Allowed
const result = await listTodos({
  fields: [
    "id",
    { user: ["name", { public_profile: ["bio"] }] }
  ]
});

// Error: user.private_settings not allowed
const result = await listTodos({
  fields: [
    "id",
    { user: ["name", { private_settings: ["data"] }] }
  ]
});

TypeScript Type Generation

Load restrictions affect the generated TypeScript types. With allowed_loads, only the allowed fields appear in the field selection types:

rpc_action :list_todos, :read, allowed_loads: [:user]
// Generated type only includes "user" as a loadable field
// "comments", "tags", etc. are not available in autocomplete
const result = await listTodos({
  fields: ["id", "title", { user: ["name"] }]  // Only user is available
});

Error Responses

When a client requests a restricted field:

// With allowed_loads: [:user]
const result = await listTodos({
  fields: ["id", { comments: ["text"] }]  // "comments" not allowed
});

// Returns:
// {
//   success: false,
//   errors: [{
//     type: "load_not_allowed",
//     message: "Field 'comments' is not in the allowed loads list",
//     fields: ["comments"]
//   }]
// }
// With denied_loads: [:internal_notes]
const result = await listTodos({
  fields: ["id", { internal_notes: ["content"] }]
});

// Returns:
// {
//   success: false,
//   errors: [{
//     type: "load_denied",
//     message: "Field 'internal_notes' is denied",
//     fields: ["internal_notes"]
//   }]
// }

When to Use Each

OptionUse When
allowed_loadsYou want explicit control over a small set of loadable fields
denied_loadsYou want to block a few sensitive fields while allowing most

Best practice: Use allowed_loads for security-sensitive endpoints where you want explicit control. Use denied_loads when most fields are safe and you only need to block a few.

Query Controls

enable_filter?

Disable client-side filtering:

typescript_rpc do
  resource MyApp.Todo do
    # Standard action with filtering
    rpc_action :list_todos, :read

    # Server controls filtering via action arguments
    rpc_action :list_recent_todos, :list_recent, enable_filter?: false
  end
end

When enable_filter?: false:

  • The filter parameter is not included in TypeScript types
  • Filter types for this action are not generated
  • Any filter sent by client is silently ignored
// With enable_filter?: false
const result = await listRecentTodos({
  fields: ["id", "title"],
  input: { daysBack: 7 }  // Use action arguments for filtering
  // filter: { ... }      // Not available in types
});

enable_sort?

Disable client-side sorting:

typescript_rpc do
  resource MyApp.Todo do
    # Standard action with sorting
    rpc_action :list_todos, :read

    # Server controls ordering
    rpc_action :list_ranked_todos, :read, enable_sort?: false
  end
end

When enable_sort?: false:

  • The sort parameter is not included in TypeScript types
  • Any sort sent by client is silently ignored
// With enable_sort?: false
const result = await listRankedTodos({
  fields: ["id", "title", "rank"]
  // sort: "-rank"  // Not available in types
});

Combining Controls

# Fully server-controlled action
rpc_action :list_curated_todos, :read,
  enable_filter?: false,
  enable_sort?: false,
  allowed_loads: [:user]

Get Actions

get?

Constrain a read action to return a single record:

typescript_rpc do
  resource MyApp.User do
    rpc_action :get_current_user, :read, get?: true
  end
end

Uses Ash.read_one instead of Ash.read, returning a single record or error.

get_by

Look up a single record by specific fields:

typescript_rpc do
  resource MyApp.User do
    rpc_action :get_user_by_email, :read, get_by: [:email]
  end
end
const result = await getUserByEmail({
  getBy: { email: "user@example.com" },
  fields: ["id", "name", "email"]
});

not_found_error?

Control behavior when a get action finds no record:

typescript_rpc do
  resource MyApp.User do
    # Returns error when not found (default)
    rpc_action :get_user, :read, get_by: [:id]

    # Returns null when not found
    rpc_action :find_user, :read, get_by: [:email], not_found_error?: false
  end
end
// With not_found_error?: false
const result = await findUser({
  getBy: { email: "maybe@example.com" },
  fields: ["id", "name"]
});

if (result.success) {
  if (result.data) {
    console.log("Found:", result.data.name);
  } else {
    console.log("User not found");  // No error, just null
  }
}

Identity Lookups

Control how records are located for update and destroy actions.

Primary Key (Default)

rpc_action :update_user, :update
# Equivalent to: identities: [:_primary_key]
await updateUser({
  identity: "550e8400-e29b-41d4-a716-446655440000",
  input: { name: "New Name" },
  fields: ["id", "name"]
});

Named Identity

First define the identity on your resource:

defmodule MyApp.User do
  use Ash.Resource

  identities do
    identity :unique_email, [:email]
  end
end

Then configure the RPC action:

rpc_action :update_user_by_email, :update, identities: [:unique_email]
await updateUserByEmail({
  identity: { email: "user@example.com" },
  input: { name: "New Name" },
  fields: ["id", "name"]
});

Multiple Identities

Allow either primary key or named identity:

rpc_action :update_user, :update, identities: [:_primary_key, :unique_email]
// By primary key
await updateUser({
  identity: "550e8400-e29b-41d4-a716-446655440000",
  input: { name: "Via PK" },
  fields: ["id"]
});

// By email
await updateUser({
  identity: { email: "user@example.com" },
  input: { name: "Via Email" },
  fields: ["id"]
});

Actor-Scoped Actions

For actions that operate on the current actor:

# Action filters to current user
defmodule MyApp.User do
  actions do
    update :update_me do
      change relate_actor(:id)
    end
  end
end

# No identity needed
rpc_action :update_me, :update_me, identities: []
// No identity parameter - operates on authenticated user
await updateMe({
  input: { name: "My New Name" },
  fields: ["id", "name"]
});

Composite Identities

Identities spanning multiple fields:

identities do
  identity :by_tenant_user, [:tenant_id, :user_id]
end

rpc_action :update_subscription, :update, identities: [:by_tenant_user]
await updateSubscription({
  identity: {
    tenantId: "tenant-uuid",
    userId: "user-uuid"
  },
  input: { status: "active" },
  fields: ["id", "status"]
});

Metadata Fields

Expose action metadata to clients:

rpc_action :list_todos, :read, show_metadata: [:total_count, :has_more]

See Action Metadata for details.

Quick Reference

OptionTypeDefaultDescription
allowed_loadslist(atom | keyword)nilWhitelist of loadable fields
denied_loadslist(atom | keyword)nilBlacklist of loadable fields
enable_filter?booleantrueEnable client-side filtering
enable_sort?booleantrueEnable client-side sorting
get?booleanfalseReturn single record
get_bylist(atom)nilFields for single-record lookup
not_found_error?booleantrueError vs null on not found
identitieslist(atom)[:_primary_key]Allowed identity lookups
show_metadatalist(atom) | false | nilnilMetadata fields to expose
metadata_field_nameskeywordnilMetadata field name mappings

Next Steps