Process Blocks and Triggers

The process block lets a post rule clean, normalize, and compute data before schema validation and permission checks.

post articles {
  process {
    $post.title = str.trim($post.title);
    $post.slug = str.slug($post.title);
  }

  schema {
    title: { type: "string", min: 1, max: 140, required: true }
    slug: { type: "string", pattern: "^[a-z0-9-]+$", required: true }
  }
}

Because process runs before the schema, the schema validates the final normalized $post.

Action-Specific Process

Use create process, edit process, delete process, or read process when one action needs extra normalization.

post articles {
  process {
    $post.title = str.trim($post.title);
  }

  create process {
    $post.slug = str.slug($post.title, str.id(8));
    $post.status = $post.status || "draft";
  }

  edit process {
    $post.updatedAt = time.now();
  }
}

Execution order is shared process, then action-specific process, then schema validation, then the matching require[...].

After Hooks

Use after create, after edit, after delete, or after read for logic that should only run after permission passes.

post products {
  after edit {
    $context.auditId = str.id(18);
  }

  create on edit[
    {
      type: "audit_logs",
      data: {
        auditId: $context.auditId,
        action: "product.edited",
        productId: $id
      }
    }
  ]
}

This keeps pre-validation cleanup in process, permission checks in require[...], and post-action side-effect preparation in after.

Assigning Values

Assignments target $post or $context.

process {
  $post.email = str.lower(str.trim($post.email));
  $post.slug = str.slug($post.title, $post.id);
  $context.auditId = str.id(16);
}

Use $post for values that should be stored. Use $context for temporary values needed later in the same rule run.

Conditional Logic

Use if, elseif, and else for branching.

process {
  if ($post.price > 100000) {
    $post.tier = "premium";
  } elseif ($post.price > 0) {
    $post.tier = "standard";
  } else {
    $post.tier = "free";
  }
}

Local Functions

Use fn for reusable logic inside the same rule file.

fn normalizeEmail(email) {
  return str.lower(str.trim(email));
}

fn makeSlug(title, id) {
  return str.slug(title, id);
}

post subscribers {
  process {
    $post.email = normalizeEmail($post.email);
    $post.slug = makeSlug($post.name, $post.id);
  }
}

Keep functions focused. If a transformation affects security, still enforce the expected final shape in schema and require[...].

Trigger Writes

Triggers let one accepted action create, edit, or delete related posts. Create triggers name the post type to create:

create on actionThatHappened[
  {
    type: "other-post-type",
    data: {
      field: "value"
    }
  }
]

Example: create an audit record whenever a product is edited.

post products {
  process {
    $context.auditId = str.id(20);
  }

  create on edit[
    {
      type: "audit_logs",
      data: {
        auditId: $context.auditId,
        action: "product.edited",
        productId: $id,
        changedBy: $user.id
      }
    }
  ]
}

The first create means the trigger will create another post. The edit means the trigger runs when the current post was edited.

Trigger Action Matrix

These are all valid trigger forms:

create on create[ ... ]
edit on create[ ... ]
delete on create[ ... ]

create on edit[ ... ]
edit on edit[ ... ]
delete on edit[ ... ]

create on delete[ ... ]
edit on delete[ ... ]
delete on delete[ ... ]

Use empty brackets when you want to reserve the block but do nothing:

delete on create[]

Trigger Payloads

Trigger payloads are FRL object expressions, so values can come from $id, $post, $prev, $user, $context, or template strings.

Create triggers only accept type and data. Frontbacked generates the created post id and author automatically.

create on create[
  {
    type: "notifications",
    data: {
      title: `New lead from ${$post.email}`,
      leadId: $id,
      notificationId: "$id"
    }
  }
]

Use $id when the related post needs the id of the post that caused the trigger. Use the exact string "$id" inside a create trigger's data when the related post needs its own generated id copied into a normal field.

Edit triggers target an existing post by id and provide the replacement data:

edit on edit[
  {
    id: $post.auditLogId,
    data: {
      action: "product.edited",
      productId: $id,
      changedBy: $user.id
    }
  }
]

Delete triggers target an existing post by id:

delete on delete[
  {
    id: $post.auditLogId
  }
]

Do not set id, authorId, status, or other post columns on create triggers. Do not set type on edit or delete triggers; post ids are already unique.

Best Practices

Use process for deterministic transformations such as trimming, lowercasing, slug creation, derived statuses, and audit ids.

Use triggers for side effects that must happen only after validation and permission checks pass.

Keep trigger payloads small and explicit. The target post type should have its own schema and permission rules too.