Action Metadata Support
View SourceAshTypescript provides full support for Ash action metadata. Action metadata allows you to expose additional computed information alongside action results, such as processing times, cache status, API versions, or any other contextual information.
Configuring Metadata Exposure
Control which metadata fields are exposed through RPC using the show_metadata option in your domain configuration:
defmodule MyApp.Domain do
use Ash.Domain, extensions: [AshTypescript.Rpc]
typescript_rpc do
resource MyApp.Task do
# Expose all metadata fields (default behavior)
rpc_action :read_with_all_metadata, :read_with_metadata, show_metadata: nil
# Disable metadata entirely
rpc_action :read_no_metadata, :read_with_metadata, show_metadata: false
# Expose specific metadata fields only
rpc_action :read_selected_metadata, :read_with_metadata,
show_metadata: [:processing_time_ms, :cache_status]
# Empty list also disables metadata
rpc_action :read_empty_metadata, :read_with_metadata, show_metadata: []
end
end
endConfiguration Options
show_metadata: nil(default) - All metadata fields from the action are exposedshow_metadata: falseor[]- Metadata is completely disabledshow_metadata: [:field1, :field2]- Only specified fields are exposed
TypeScript Usage
Read Actions (Metadata Merged into Records)
For read actions, metadata fields are merged directly into each record:
import { readWithAllMetadata } from './ash_rpc';
// Select which metadata fields to include
const tasks = await readWithAllMetadata({
fields: ["id", "title"],
metadataFields: ["processingTimeMs", "cacheStatus", "apiVersion"]
});
if (tasks.success) {
tasks.data.forEach(task => {
console.log(task.id); // Standard field
console.log(task.title); // Standard field
console.log(task.processingTimeMs); // Metadata field (merged in)
console.log(task.cacheStatus); // Metadata field (merged in)
console.log(task.apiVersion); // Metadata field (merged in)
});
}
// Select subset of metadata fields
const tasksSubset = await readWithAllMetadata({
fields: ["id", "title"],
metadataFields: ["cacheStatus"] // Only request specific metadata
});
// Omit metadataFields to not include any metadata
const tasksNoMetadata = await readWithAllMetadata({
fields: ["id", "title"]
// No metadataFields = no metadata included
});Mutation Actions (Metadata as Separate Field)
For create, update, and destroy actions, metadata is returned as a separate metadata field:
import { createTask } from './ash_rpc';
const result = await createTask({
fields: ["id", "title"],
input: { title: "New Task" }
});
if (result.success) {
// Access the created task
console.log(result.data.id);
console.log(result.data.title);
// Access metadata separately
console.log(result.metadata.operationId); // Metadata field
console.log(result.metadata.createdAtServer); // Metadata field
}Selective Metadata Field Selection
When show_metadata exposes specific fields, only those fields can be selected:
# Only :processing_time_ms and :cache_status are exposed
rpc_action :read_limited, :read_with_metadata,
show_metadata: [:processing_time_ms, :cache_status]// ✅ Allowed: Request exposed fields
const tasks = await readLimited({
fields: ["id", "title"],
metadataFields: ["processingTimeMs", "cacheStatus"]
});
// ✅ Allowed: Request subset of exposed fields
const tasksPartial = await readLimited({
fields: ["id", "title"],
metadataFields: ["processingTimeMs"]
});
// ⚠️ Silently filtered: Non-exposed fields are ignored
const tasksFiltered = await readLimited({
fields: ["id", "title"],
metadataFields: ["processingTimeMs", "apiVersion"] // apiVersion not exposed
});
// Result will only include processingTimeMs, apiVersion is filtered outField Name Formatting
Metadata field names follow the same formatting rules as regular fields:
# Elixir: snake_case
metadata :processing_time_ms, :integer
metadata :cache_status, :string// TypeScript: camelCase (with default formatter)
result.metadata.processingTimeMs // Formatted
result.metadata.cacheStatus // FormattedType Safety
Generated TypeScript types include metadata fields with full type inference:
// For read actions with metadata merged in
type TaskWithMetadata = {
id: string;
title: string;
processingTimeMs?: number | null; // Metadata field
cacheStatus?: string | null; // Metadata field
apiVersion?: string | null; // Metadata field
}
// For mutations with separate metadata
type CreateTaskResult = {
success: true;
data: {
id: string;
title: string;
};
metadata: {
operationId: string;
createdAtServer: string;
}
} | {
success: false;
errors: Array<ErrorType>;
}Metadata Field Name Mapping
TypeScript has stricter identifier rules than Elixir. If your action's metadata fields use invalid TypeScript names, use the metadata_field_names option to map them to valid identifiers.
Invalid Metadata Field Name Patterns
- Underscores before digits:
field_1,metric_2,item__3 - Question marks:
is_cached?,valid?
Mapping Invalid Names
Map invalid metadata field names using the metadata_field_names option:
defmodule MyApp.Domain do
use Ash.Domain, extensions: [AshTypescript.Rpc]
typescript_rpc do
resource MyApp.Task do
rpc_action :read_with_metadata, :read_with_metadata,
show_metadata: [:field_1, :is_cached?, :metric_2],
metadata_field_names: [
field_1: :field1,
is_cached?: :isCached,
metric_2: :metric2
]
end
end
endGenerated TypeScript with Mapped Names
// Read actions - metadata merged into records
const tasks = await readWithMetadata({
fields: ["id", "title"],
metadataFields: ["field1", "isCached", "metric2"] // Mapped names
});
if (tasks.success) {
tasks.data.forEach(task => {
console.log(task.id); // Standard field
console.log(task.title); // Standard field
console.log(task.field1); // Mapped metadata field
console.log(task.isCached); // Mapped metadata field
console.log(task.metric2); // Mapped metadata field
});
}
// Create/Update/Destroy actions - metadata as separate field
const result = await createTask({
fields: ["id", "title"],
input: { title: "New Task" }
});
if (result.success) {
console.log(result.data.id);
console.log(result.metadata.field1); // Mapped metadata field
console.log(result.metadata.isCached); // Mapped metadata field
}Compile-time Verification
AshTypescript includes compile-time verification that detects invalid metadata field names:
Invalid metadata field name found in action :read_with_metadata on resource MyApp.Task
Metadata field 'field_1' contains invalid pattern (underscore before digit).
Suggested mapping: field_1 → field1
Metadata field 'is_cached?' contains invalid pattern (question mark).
Suggested mapping: is_cached? → isCached
Use the metadata_field_names option to provide valid TypeScript identifiers.See Also
- Troubleshooting Guide - Learn about field and argument name mapping
- Ash Action Metadata - Learn about Ash metadata in depth