# "Process Groups"
_Path: en/lua/core/pg_
> "Join processes into named groups and broadcast to every member across the cluster. Modeled on Erlang/OTP pg: groups are dynamic, a process can belong…"
## Table of Contents
- Process Groups
## Content
# Process Groups
Process groups organize processes under dynamic names and broadcast messages to group members across the cluster. A process can join multiple groups, and cluster-wide membership is eventually consistent.
This is an API reference. Its snippets assume an existing `pg.scope`, an executable entry running with process context, and policies that authorize the documented operations. The blocks demonstrate individual calls or partial subscription flows rather than a standalone application.
For the scope entry kind and its configuration, see [Process Groups](system/process-groups.md). For the broader clustering model, see the [Cluster Guide](guides/cluster.md).
## Loading
```lua
local pg = require("pg")
```
Add `pg` to the executable entry's `modules:` list before requiring it.
## Opening a Scope
A process group belongs to a **scope**, represented by a `pg.scope` registry entry. Open the scope to obtain an instance for group operations:
```lua
local group, err = pg.open("app:pg")
if err then
return nil, err
end
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | string | Scope entry ID (format: `"namespace:name"`) |
**Returns:** `pg.Instance, error`
**Permission:** `pg.open` on the scope `id`
The instance is released automatically during execution-frame cleanup. Call `release()` to release it earlier. Other operations are methods on the instance and use `:` syntax.
## Joining and Leaving
The calls below are independent forms; choose the single-group or batch join needed by the application and pair it with the corresponding leave operations.
```lua
local ok, err = group:join("workers") -- single group
if err then return nil, err end
```
```lua
local ok, err = group:join({"workers", "all"}) -- batch
if err then return nil, err end
```
```lua
local ok, err = group:leave("workers")
if err then return nil, err end
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `group` | string \| string[] | Group name, or a list of names for a batch operation |
**Returns:** `boolean, error`
A process can join the same group more than once and must leave the same number of times to depart fully. For a batch, `leave` is best-effort and returns an error only when the process was not a member of any named group.
**Permissions:** `pg.join` / `pg.leave` on each group name
## Listing Members
```lua
local members, err = group:get_members("workers") -- all nodes
if err then return nil, err end
local local_members, err = group:get_local_members("workers") -- this node only
if err then return nil, err end
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `group` | string | Group name |
**Returns:** `string[], error` — an array of PID strings (empty for an unknown group)
**Permissions:** `pg.get_members` / `pg.get_local_members` on the group name
## Listing Groups
```lua
local groups, err = group:which_groups() -- all groups in the cluster
if err then return nil, err end
local local_groups, err = group:which_local_groups() -- groups with a local member
if err then return nil, err end
```
**Returns:** `string[], error` — group names that currently have at least one member
**Permissions:** `pg.which_groups` / `pg.which_local_groups`
## Broadcasting
Broadcast sends a message from the calling process to every group member under `topic`. Members receive it with `process.listen(topic)`.
```lua
local ok, err = group:broadcast("workers", "task", {id = 42}) -- all nodes
if err then return nil, err end
ok, err = group:broadcast_local("workers", "task", {id = 42}) -- this node only
if err then return nil, err end
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `group` | string | Target group |
| `topic` | string | Message topic |
| `...` | any | Zero or more payload values |
**Returns:** `boolean, error`
**Permissions:** `pg.broadcast` / `pg.broadcast_local` on the group name
## Monitoring a Group
`monitor` subscribes to join and leave events for one group and returns an atomic snapshot of its current members. No membership change can occur between the snapshot and subscription setup without being observed.
```lua
local sub, members, err = group:monitor("workers")
if err then
return nil, err
end
for _, pid in ipairs(members) do
-- current members at subscription time
end
local ch = sub:channel()
local event, open = ch:receive() -- {kind = "member.joined" | "member.left", path = "workers", data = {...}}
if not open then
return nil, errors.new("Process-group subscription closed")
end
sub:close() -- unsubscribe; sub:close({flush = true}) drains queued events first
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `group` | string | Group to watch |
**Returns:** `pg.Subscription, string[], error` — the subscription and a snapshot of current members
**Permission:** `pg.monitor` on the group name
## Watching All Groups
`events` subscribes to membership changes for every group in the scope and returns a snapshot mapping groups to their members.
```lua
local sub, snapshot, err = group:events()
if err then
return nil, err
end
-- snapshot: { ["workers"] = {pid, ...}, ["all"] = {pid, ...} }
local event, open = sub:channel():receive()
if not open then
return nil, errors.new("Process-group subscription closed")
end
sub:close()
```
**Returns:** `pg.Subscription, table, error`
**Permission:** `pg.events`
### Event Fields
Events delivered on a subscription channel carry:
| Field | Type | Description |
|-------|------|-------------|
| `system` | string | Always `"pg"` |
| `kind` | string | `"member.joined"` or `"member.left"` |
| `path` | string | The group name |
| `data` | table | `{Group = string, PIDs = string[]}` — the affected members |
Subscription channels are buffered (capacity 64). If a slow consumer fills the buffer, further events are retained in the process mailbox in order and delivered once the consumer drains the channel (the subscription stalls rather than dropping events).
## Releasing
```lua
group:release()
```
`release` frees the instance immediately and is idempotent. After release, every other group operation returns an error. Cleanup also runs automatically at the end of the execution frame.
**Returns:** `boolean`
## Permissions
| Permission | Method | Resource |
|------------|--------|----------|
| `pg.open` | `pg.open()` | scope id |
| `pg.join` | `join()` | group name |
| `pg.leave` | `leave()` | group name |
| `pg.get_members` | `get_members()` | group name |
| `pg.get_local_members` | `get_local_members()` | group name |
| `pg.which_groups` | `which_groups()` | (none) |
| `pg.which_local_groups` | `which_local_groups()` | (none) |
| `pg.broadcast` | `broadcast()` | group name |
| `pg.broadcast_local` | `broadcast_local()` | group name |
| `pg.monitor` | `monitor()` | group name |
| `pg.events` | `events()` | (none) |
## Errors
| Condition | Kind |
|-----------|------|
| Permission denied | `errors.PERMISSION_DENIED` |
| Missing or empty argument | `errors.INVALID` |
| Scope not found | `errors.INTERNAL` |
| Leave a group with no membership | `errors.NOT_FOUND` |
| Instance released | `errors.INVALID` |
| Group/member or action-queue limit reached | `errors.RATE_LIMITED` (retryable) |
| Service stopped, backpressure, or open circuit | `errors.UNAVAILABLE` |
| Broadcast timed out | `errors.TIMEOUT` (retryable) |
See [Error Handling](lua/core/errors.md) for working with errors.
## See Also
- [Process Groups](system/process-groups.md) - Scope entry kind and configuration
- [Cluster](guides/cluster.md) - Membership, naming, and the clustering model
- [Process Management](lua/core/process.md) - Spawning and messaging individual processes
## Navigation
Previous: "Process Management" (lua/core/process)
Next: "Function Invocation" (lua/core/funcs)