Search Docs
Loading...
Skip to content

Editing Workflow

CE.SDK controls editing access through roles and scopes, enabling template workflows where designers create locked layouts and end-users customize only the permitted parts.

5 mins
estimated time
GitHub

The Kotlin snippets below assume you already have an Engine instance on the main thread.

CE.SDK uses a two-tier permission system: roles define user types with preset permissions, while scopes control specific capabilities. This enables workflows where templates can be prepared by designers and safely customized by end-users.

This guide covers:

  • The four user roles and their purposes
  • How scopes control editing capabilities
  • The permission resolution hierarchy
  • Common template workflow patterns

Roles#

Roles define user types with different default permissions:

RolePurposeDefault Access
CreatorDesigners building templatesFull access to all operations
AdopterEnd-users customizing templatesLimited by block-level scopes
ViewerStatic preview without interactionRead-only, no playback controls
PresenterPresenting slideshows or playing videosRead-only with playback and navigation

Creators set the block-level scopes that constrain what Adopters can do. This separation enables brand consistency while allowing personalization.

// Roles define user types: "Creator", "Adopter", "Viewer", "Presenter".
val role = engine.editor.getRole()
println("Current role: $role") // "Creator"
engine.editor.setRole("Adopter")
val adopterRole = engine.editor.getRole()
println("Preview role: $adopterRole") // "Adopter"
engine.editor.setRole("Creator")

Scopes#

Scopes define specific capabilities organized into categories:

  • Text: Editing content and character formatting
  • Fill/Stroke: Changing colors and shapes
  • Layer: Moving, resizing, rotating, cropping
  • Appearance: Filters, effects, shadows, animations
  • Lifecycle: Deleting and duplicating elements
  • Editor: Adding new elements and selecting

Global vs Block-Level Scopes#

Global scopes apply editor-wide and determine whether block-level settings are checked:

  • GlobalScope.ALLOW — Always permit the operation
  • GlobalScope.DENY — Always block the operation
  • GlobalScope.DEFER — Check block-level scope settings

Block-level scopes control permissions on individual blocks. These settings only take effect when the corresponding global scope is set to GlobalScope.DEFER.

// Defer to the block-level settings so the template controls the Adopter experience.
engine.editor.setGlobalScope(key = "editor/select", globalScope = GlobalScope.DEFER)
engine.editor.setGlobalScope(key = "layer/move", globalScope = GlobalScope.DEFER)
engine.editor.setGlobalScope(key = "text/edit", globalScope = GlobalScope.DEFER)
engine.editor.setGlobalScope(key = "text/character", globalScope = GlobalScope.DEFER)
engine.editor.setGlobalScope(key = "lifecycle/destroy", globalScope = GlobalScope.DEFER)
val moveScope = engine.editor.getGlobalScope(key = "layer/move")
val allScopes = engine.editor.findAllScopes()
println("Global 'layer/move' scope: $moveScope")
println("Available scopes: ${allScopes.count()}")

To lock a specific block, disable its scopes:

engine.block.setScopeEnabled(page, key = "editor/select", enabled = false)
engine.block.setScopeEnabled(page, key = "layer/move", enabled = false)
engine.block.setScopeEnabled(page, key = "lifecycle/destroy", enabled = false)
engine.block.setScopeEnabled(background, key = "editor/select", enabled = false)
engine.block.setScopeEnabled(background, key = "layer/move", enabled = false)
engine.block.setScopeEnabled(background, key = "lifecycle/destroy", enabled = false)
// Locked brand elements stay fixed for Adopters.
engine.block.setScopeEnabled(brandBanner, key = "editor/select", enabled = false)
engine.block.setScopeEnabled(brandBanner, key = "layer/move", enabled = false)
engine.block.setScopeEnabled(brandBanner, key = "lifecycle/destroy", enabled = false)
engine.block.setScopeEnabled(companyName, key = "editor/select", enabled = false)
engine.block.setScopeEnabled(companyName, key = "layer/move", enabled = false)
engine.block.setScopeEnabled(companyName, key = "text/edit", enabled = false)
engine.block.setScopeEnabled(companyName, key = "text/character", enabled = false)
engine.block.setScopeEnabled(companyName, key = "lifecycle/destroy", enabled = false)
// Keep the attendee name editable but fixed in place.
engine.block.setScopeEnabled(attendeeName, key = "editor/select", enabled = true)
engine.block.setScopeEnabled(attendeeName, key = "layer/move", enabled = false)
engine.block.setScopeEnabled(attendeeName, key = "text/edit", enabled = true)
engine.block.setScopeEnabled(attendeeName, key = "text/character", enabled = false)
engine.block.setScopeEnabled(attendeeName, key = "lifecycle/destroy", enabled = false)

In the example template, the brand banner and company name are locked while the attendee name keeps editor/select and text/edit enabled so adopters can personalize the template without moving or deleting anything.

Permission Resolution#

Permissions resolve in this order:

  1. Role defaults — Each role has preset global scope values
  2. Global scope — If GlobalScope.ALLOW or GlobalScope.DENY, this is the final answer
  3. Block-level scope — If global is GlobalScope.DEFER, check the block’s settings

Use isAllowedByScope() to check the final computed permission for any block and scope combination:

engine.editor.setRole("Creator")
val creatorCanSelectBrandBanner = engine.block.isAllowedByScope(brandBanner, key = "editor/select")
val creatorCanEditAttendeeName = engine.block.isAllowedByScope(attendeeName, key = "text/edit")
println("Creator can select the banner: $creatorCanSelectBrandBanner") // true
println("Creator can edit the attendee name: $creatorCanEditAttendeeName") // true

Switching Roles#

Change roles at runtime with setRole(). When switching to Adopter, block-level restrictions take effect. Switching back to Creator restores full access.

engine.editor.setRole("Adopter")
val adopterCanSelectBrandBanner = engine.block.isAllowedByScope(brandBanner, key = "editor/select")
val adopterCanEditAttendeeName = engine.block.isAllowedByScope(attendeeName, key = "text/edit")
println("Adopter can select the banner: $adopterCanSelectBrandBanner") // false
println("Adopter can edit the attendee name: $adopterCanEditAttendeeName") // true
engine.editor.setRole("Creator")

Customizing Role Behavior#

onRoleChanged() returns a Flow<String> that emits after role defaults are applied. Collect it before switching roles when you need to override selected scopes for a role:

fun customizeEditingWorkflowRoles(
engine: Engine,
scope: CoroutineScope,
): Job = scope.launch(start = CoroutineStart.UNDISPATCHED) {
engine.editor.onRoleChanged().collect { role ->
if (role == "Adopter") {
engine.editor.setGlobalScope(key = "appearance/filter", globalScope = GlobalScope.ALLOW)
engine.editor.setGlobalScope(key = "appearance/effect", globalScope = GlobalScope.ALLOW)
}
}
}

Template Workflow Pattern#

A typical template workflow:

  1. Designer (Creator) creates the template layout
  2. Designer locks brand elements using block scopes
  3. Designer keeps personalization fields editable
  4. End-user (Adopter) opens the template
  5. End-user edits only permitted elements
  6. End-user exports the personalized result

This pattern ensures brand consistency while enabling personalization. The Android example creates a small template with locked brand elements and one editable personalization field:

val brandBanner = engine.block.create(DesignBlockType.Graphic)
engine.block.setName(brandBanner, BRAND_BANNER_NAME)
engine.block.setShape(brandBanner, shape = engine.block.createShape(ShapeType.Rect))
engine.block.setWidth(brandBanner, value = 640F)
engine.block.setHeight(brandBanner, value = 220F)
engine.block.setPositionX(brandBanner, value = 40F)
engine.block.setPositionY(brandBanner, value = 48F)
engine.block.setFill(brandBanner, fill = engine.block.createFill(FillType.Color))
engine.block.setColor(
block = engine.block.getFill(brandBanner),
property = "fill/color/value",
value = Color.fromHex("#FF0B1220"),
)
engine.block.appendChild(parent = page, child = brandBanner)
val companyName = engine.block.create(DesignBlockType.Text)
engine.block.setName(companyName, COMPANY_NAME)
engine.block.setWidthMode(companyName, mode = SizeMode.AUTO)
engine.block.setHeightMode(companyName, mode = SizeMode.AUTO)
engine.block.setPositionX(companyName, value = 88F)
engine.block.setPositionY(companyName, value = 122F)
engine.block.replaceText(companyName, text = "IMGLY Labs")
engine.block.setTextColor(companyName, color = Color.fromHex("#FFFFFFFF"))
engine.block.appendChild(parent = page, child = companyName)
val attendeeName = engine.block.create(DesignBlockType.Text)
engine.block.setName(attendeeName, ATTENDEE_NAME)
engine.block.setWidthMode(attendeeName, mode = SizeMode.AUTO)
engine.block.setHeightMode(attendeeName, mode = SizeMode.AUTO)
engine.block.setPositionX(attendeeName, value = 88F)
engine.block.setPositionY(attendeeName, value = 404F)
engine.block.replaceText(attendeeName, text = "Alex Morgan")
engine.block.setTextColor(attendeeName, color = Color.fromHex("#FF0B1220"))
engine.block.setBackgroundColor(attendeeName, color = Color.fromRGBA(231, 240, 255, 255))
engine.block.setBackgroundColorEnabled(attendeeName, enabled = true)
engine.block.setFloat(attendeeName, property = "backgroundColor/paddingLeft", value = 24F)
engine.block.setFloat(attendeeName, property = "backgroundColor/paddingTop", value = 20F)
engine.block.setFloat(attendeeName, property = "backgroundColor/paddingRight", value = 24F)
engine.block.setFloat(attendeeName, property = "backgroundColor/paddingBottom", value = 20F)
engine.block.setFloat(attendeeName, property = "backgroundColor/cornerRadius", value = 18F)
engine.block.appendChild(parent = page, child = attendeeName)

Troubleshooting#

  • Block-level restrictions do not apply — Set the matching global scope to GlobalScope.DEFER; GlobalScope.ALLOW and GlobalScope.DENY bypass block settings.
  • Role-specific overrides disappear after switching roles — Apply custom scope changes from onRoleChanged() because role defaults are applied during the role switch.
  • A block cannot be selected at all — Check editor/select; disabling that scope prevents interaction before other scope checks can matter.

API Reference#

APIPurpose
engine.editor.setRole(role=_)Set the active user role.
engine.editor.getRole()Read the active user role.
engine.editor.onRoleChanged()Collect role changes after role defaults are applied.
engine.editor.setGlobalScope(key="layer/move",globalScope=_)Allow, deny, or defer an operation globally.
engine.editor.getGlobalScope(key="layer/move")Read the global state for one scope.
engine.editor.findAllScopes()List all scope keys supported by the engine.
engine.block.setScopeEnabled(block=_,key="text/edit",enabled=_)Enable or disable one scope on a block.
engine.block.isScopeEnabled(block=_,key="text/edit")Read the block-level scope flag.
engine.block.isAllowedByScope(block=_,key="text/edit")Check the final resolved permission for a block.

Next Steps#